Completed
Push — develop ( 36d39c...5e31dd )
by Juliette
04:10
created

class-tgm-plugin-activation.php (2 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/**
3
 * Plugin installation and activation for WordPress themes.
4
 *
5
 * Please note that this is a drop-in library for a theme or plugin.
6
 * The authors of this library (Thomas, Gary and Juliette) are NOT responsible
7
 * for the support of your plugin or theme. Please contact the plugin
8
 * or theme author for support.
9
 *
10
 * @package   TGM-Plugin-Activation
11
 * @version   2.5.2
12
 * @link      http://tgmpluginactivation.com/
13
 * @author    Thomas Griffin, Gary Jones, Juliette Reinders Folmer
14
 * @copyright Copyright (c) 2011, Thomas Griffin
15
 * @license   GPL-2.0+
16
 */
17
18
/*
19
	Copyright 2011 Thomas Griffin (thomasgriffinmedia.com)
20
21
	This program is free software; you can redistribute it and/or modify
22
	it under the terms of the GNU General Public License, version 2, as
23
	published by the Free Software Foundation.
24
25
	This program is distributed in the hope that it will be useful,
26
	but WITHOUT ANY WARRANTY; without even the implied warranty of
27
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
28
	GNU General Public License for more details.
29
30
	You should have received a copy of the GNU General Public License
31
	along with this program; if not, write to the Free Software
32
	Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
33
*/
34
35
if ( ! class_exists( 'TGM_Plugin_Activation' ) ) {
36
37
	/**
38
	 * Automatic plugin installation and activation library.
39
	 *
40
	 * Creates a way to automatically install and activate plugins from within themes.
41
	 * The plugins can be either bundled, downloaded from the WordPress
42
	 * Plugin Repository or downloaded from another external source.
43
	 *
44
	 * @since 1.0.0
45
	 *
46
	 * @package TGM-Plugin-Activation
47
	 * @author  Thomas Griffin
48
	 * @author  Gary Jones
49
	 */
50
	class TGM_Plugin_Activation {
51
		/**
52
		 * TGMPA version number.
53
		 *
54
		 * @since 2.5.0
55
		 *
56
		 * @const string Version number.
57
		 */
58
		const TGMPA_VERSION = '2.5.2';
59
60
		/**
61
		 * Regular expression to test if a URL is a WP plugin repo URL.
62
		 *
63
		 * @const string Regex.
64
		 *
65
		 * @since 2.5.0
66
		 */
67
		const WP_REPO_REGEX = '|^http[s]?://wordpress\.org/(?:extend/)?plugins/|';
68
69
		/**
70
		 * Arbitrary regular expression to test if a string starts with a URL.
71
		 *
72
		 * @const string Regex.
73
		 *
74
		 * @since 2.5.0
75
		 */
76
		const IS_URL_REGEX = '|^http[s]?://|';
77
78
		/**
79
		 * Holds a copy of itself, so it can be referenced by the class name.
80
		 *
81
		 * @since 1.0.0
82
		 *
83
		 * @var TGM_Plugin_Activation
84
		 */
85
		public static $instance;
86
87
		/**
88
		 * Holds arrays of plugin details.
89
		 *
90
		 * @since 1.0.0
91
		 *
92
		 * @since 2.5.0 the array has the plugin slug as an associative key.
93
		 *
94
		 * @var array
95
		 */
96
		public $plugins = array();
97
98
		/**
99
		 * Holds arrays of plugin names to use to sort the plugins array.
100
		 *
101
		 * @since 2.5.0
102
		 *
103
		 * @var array
104
		 */
105
		protected $sort_order = array();
106
107
		/**
108
		 * Whether any plugins have the 'force_activation' setting set to true.
109
		 *
110
		 * @since 2.5.0
111
		 *
112
		 * @var bool
113
		 */
114
		protected $has_forced_activation = false;
115
116
		/**
117
		 * Whether any plugins have the 'force_deactivation' setting set to true.
118
		 *
119
		 * @since 2.5.0
120
		 *
121
		 * @var bool
122
		 */
123
		protected $has_forced_deactivation = false;
124
125
		/**
126
		 * Name of the unique ID to hash notices.
127
		 *
128
		 * @since 2.4.0
129
		 *
130
		 * @var string
131
		 */
132
		public $id = 'tgmpa';
133
134
		/**
135
		 * Name of the query-string argument for the admin page.
136
		 *
137
		 * @since 1.0.0
138
		 *
139
		 * @var string
140
		 */
141
		protected $menu = 'tgmpa-install-plugins';
142
143
		/**
144
		 * Parent menu file slug.
145
		 *
146
		 * @since 2.5.0
147
		 *
148
		 * @var string
149
		 */
150
		public $parent_slug = 'themes.php';
151
152
		/**
153
		 * Capability needed to view the plugin installation menu item.
154
		 *
155
		 * @since 2.5.0
156
		 *
157
		 * @var string
158
		 */
159
		public $capability = 'edit_theme_options';
160
161
		/**
162
		 * Default absolute path to folder containing bundled plugin zip files.
163
		 *
164
		 * @since 2.0.0
165
		 *
166
		 * @var string Absolute path prefix to zip file location for bundled plugins. Default is empty string.
167
		 */
168
		public $default_path = '';
169
170
		/**
171
		 * Flag to show admin notices or not.
172
		 *
173
		 * @since 2.1.0
174
		 *
175
		 * @var boolean
176
		 */
177
		public $has_notices = true;
178
179
		/**
180
		 * Flag to determine if the user can dismiss the notice nag.
181
		 *
182
		 * @since 2.4.0
183
		 *
184
		 * @var boolean
185
		 */
186
		public $dismissable = true;
187
188
		/**
189
		 * Message to be output above nag notice if dismissable is false.
190
		 *
191
		 * @since 2.4.0
192
		 *
193
		 * @var string
194
		 */
195
		public $dismiss_msg = '';
196
197
		/**
198
		 * Flag to set automatic activation of plugins. Off by default.
199
		 *
200
		 * @since 2.2.0
201
		 *
202
		 * @var boolean
203
		 */
204
		public $is_automatic = false;
205
206
		/**
207
		 * Optional message to display before the plugins table.
208
		 *
209
		 * @since 2.2.0
210
		 *
211
		 * @var string Message filtered by wp_kses_post(). Default is empty string.
212
		 */
213
		public $message = '';
214
215
		/**
216
		 * Holds configurable array of strings.
217
		 *
218
		 * Default values are added in the constructor.
219
		 *
220
		 * @since 2.0.0
221
		 *
222
		 * @var array
223
		 */
224
		public $strings = array();
225
226
		/**
227
		 * Holds the version of WordPress.
228
		 *
229
		 * @since 2.4.0
230
		 *
231
		 * @var int
232
		 */
233
		public $wp_version;
234
235
		/**
236
		 * Holds the hook name for the admin page.
237
		 *
238
		 * @since 2.5.0
239
		 *
240
		 * @var string
241
		 */
242
		public $page_hook;
243
244
		/**
245
		 * Adds a reference of this object to $instance, populates default strings,
246
		 * does the tgmpa_init action hook, and hooks in the interactions to init.
247
		 *
248
		 * {@internal This method should be `protected`, but as too many TGMPA implementations
249
		 * haven't upgraded beyond v2.3.6 yet, this gives backward compatibility issues.
250
		 * Reverted back to public for the time being.}}
251
		 *
252
		 * @since 1.0.0
253
		 *
254
		 * @see TGM_Plugin_Activation::init()
255
		 */
256
		public function __construct() {
257
			// Set the current WordPress version.
258
			$this->wp_version = $GLOBALS['wp_version'];
259
260
			// Announce that the class is ready, and pass the object (for advanced use).
261
			do_action_ref_array( 'tgmpa_init', array( $this ) );
262
263
			// When the rest of WP has loaded, kick-start the rest of the class.
264
			add_action( 'init', array( $this, 'init' ) );
265
		}
266
267
		/**
268
		 * Magic method to (not) set protected properties from outside of this class.
269
		 *
270
		 * {@internal hackedihack... There is a serious bug in v2.3.2 - 2.3.6  where the `menu` property
271
		 * is being assigned rather than tested in a conditional, effectively rendering it useless.
272
		 * This 'hack' prevents this from happening.}}
273
		 *
274
		 * @see https://github.com/TGMPA/TGM-Plugin-Activation/blob/2.3.6/tgm-plugin-activation/class-tgm-plugin-activation.php#L1593
275
		 *
276
		 * @param string $name  Name of an inaccessible property.
277
		 * @param mixed  $value Value to assign to the property.
278
		 * @return void  Silently fail to set the property when this is tried from outside of this class context.
279
		 *               (Inside this class context, the __set() method if not used as there is direct access.)
280
		 */
281
		public function __set( $name, $value ) {
282
			return;
283
		}
284
285
		/**
286
		 * Magic method to get the value of a protected property outside of this class context.
287
		 *
288
		 * @param string $name Name of an inaccessible property.
289
		 * @return mixed The property value.
290
		 */
291
		public function __get( $name ) {
292
			return $this->{$name};
293
		}
294
295
		/**
296
		 * Initialise the interactions between this class and WordPress.
297
		 *
298
		 * Hooks in three new methods for the class: admin_menu, notices and styles.
299
		 *
300
		 * @since 2.0.0
301
		 *
302
		 * @see TGM_Plugin_Activation::admin_menu()
303
		 * @see TGM_Plugin_Activation::notices()
304
		 * @see TGM_Plugin_Activation::styles()
305
		 */
306
		public function init() {
307
			/**
308
			 * By default TGMPA only loads on the WP back-end and not in an Ajax call. Using this filter
309
			 * you can overrule that behaviour.
310
			 *
311
			 * @since 2.5.0
312
			 *
313
			 * @param bool $load Whether or not TGMPA should load.
314
			 *                   Defaults to the return of `is_admin() && ! defined( 'DOING_AJAX' )`.
315
			 */
316
			if ( true !== apply_filters( 'tgmpa_load', ( is_admin() && ! defined( 'DOING_AJAX' ) ) ) ) {
317
				return;
318
			}
319
320
			// Load class strings.
321
			$this->strings = array(
322
				'page_title'                      => __( 'Install Required Plugins', 'tgmpa' ),
323
				'menu_title'                      => __( 'Install Plugins', 'tgmpa' ),
324
				'installing'                      => __( 'Installing Plugin: %s', 'tgmpa' ),
325
				'updating'                        => __( 'Updating Plugin: %s', 'tgmpa' ),
326
				'oops'                            => __( 'Something went wrong with the plugin API.', 'tgmpa' ),
327
				'notice_can_install_required'     => _n_noop(
328
					'This theme requires the following plugin: %1$s.',
329
					'This theme requires the following plugins: %1$s.',
330
					'tgmpa'
331
				),
332
				'notice_can_install_recommended'  => _n_noop(
333
					'This theme recommends the following plugin: %1$s.',
334
					'This theme recommends the following plugins: %1$s.',
335
					'tgmpa'
336
				),
337
				'notice_ask_to_update'            => _n_noop(
338
					'The following plugin needs to be updated to its latest version to ensure maximum compatibility with this theme: %1$s.',
339
					'The following plugins need to be updated to their latest version to ensure maximum compatibility with this theme: %1$s.',
340
					'tgmpa'
341
				),
342
				'notice_ask_to_update_maybe'      => _n_noop(
343
					'There is an update available for: %1$s.',
344
					'There are updates available for the following plugins: %1$s.',
345
					'tgmpa'
346
				),
347
				'notice_can_activate_required'    => _n_noop(
348
					'The following required plugin is currently inactive: %1$s.',
349
					'The following required plugins are currently inactive: %1$s.',
350
					'tgmpa'
351
				),
352
				'notice_can_activate_recommended' => _n_noop(
353
					'The following recommended plugin is currently inactive: %1$s.',
354
					'The following recommended plugins are currently inactive: %1$s.',
355
					'tgmpa'
356
				),
357
				'install_link'                    => _n_noop(
358
					'Begin installing plugin',
359
					'Begin installing plugins',
360
					'tgmpa'
361
				),
362
				'update_link'                     => _n_noop(
363
					'Begin updating plugin',
364
					'Begin updating plugins',
365
					'tgmpa'
366
				),
367
				'activate_link'                   => _n_noop(
368
					'Begin activating plugin',
369
					'Begin activating plugins',
370
					'tgmpa'
371
				),
372
				'return'                          => __( 'Return to Required Plugins Installer', 'tgmpa' ),
373
				'dashboard'                       => __( 'Return to the dashboard', 'tgmpa' ),
374
				'plugin_activated'                => __( 'Plugin activated successfully.', 'tgmpa' ),
375
				'activated_successfully'          => __( 'The following plugin was activated successfully:', 'tgmpa' ),
376
				'plugin_already_active'           => __( 'No action taken. Plugin %1$s was already active.', 'tgmpa' ),
377
				'plugin_needs_higher_version'     => __( 'Plugin not activated. A higher version of %s is needed for this theme. Please update the plugin.', 'tgmpa' ),
378
				'complete'                        => __( 'All plugins installed and activated successfully. %1$s', 'tgmpa' ),
379
				'dismiss'                         => __( 'Dismiss this notice', 'tgmpa' ),
380
				'notice_cannot_install_activate'  => __( 'There are one or more required or recommended plugins to install, update or activate.', 'tgmpa' ),
381
				'contact_admin'                   => __( 'Please contact the administrator of this site for help.', 'tgmpa' ),
382
			);
383
384
			do_action( 'tgmpa_register' );
385
386
			/* After this point, the plugins should be registered and the configuration set. */
387
388
			// Proceed only if we have plugins to handle.
389
			if ( empty( $this->plugins ) || ! is_array( $this->plugins ) ) {
390
				return;
391
			}
392
393
			// Set up the menu and notices if we still have outstanding actions.
394
			if ( true !== $this->is_tgmpa_complete() ) {
395
				// Sort the plugins.
396
				array_multisort( $this->sort_order, SORT_ASC, $this->plugins );
397
398
				add_action( 'admin_menu', array( $this, 'admin_menu' ) );
399
				add_action( 'admin_head', array( $this, 'dismiss' ) );
400
401
				// Prevent the normal links from showing underneath a single install/update page.
402
				add_filter( 'install_plugin_complete_actions', array( $this, 'actions' ) );
403
				add_filter( 'update_plugin_complete_actions', array( $this, 'actions' ) );
404
405
				if ( $this->has_notices ) {
406
					add_action( 'admin_notices', array( $this, 'notices' ) );
407
					add_action( 'admin_init', array( $this, 'admin_init' ), 1 );
408
					add_action( 'admin_enqueue_scripts', array( $this, 'thickbox' ) );
409
				}
410
			}
411
412
			// If needed, filter plugin action links.
413
			add_action( 'load-plugins.php', array( $this, 'add_plugin_action_link_filters' ), 1 );
414
415
			// Make sure things get reset on switch theme.
416
			add_action( 'switch_theme', array( $this, 'flush_plugins_cache' ) );
417
418
			if ( $this->has_notices ) {
419
				add_action( 'switch_theme', array( $this, 'update_dismiss' ) );
420
			}
421
422
			// Setup the force activation hook.
423
			if ( true === $this->has_forced_activation ) {
424
				add_action( 'admin_init', array( $this, 'force_activation' ) );
425
			}
426
427
			// Setup the force deactivation hook.
428
			if ( true === $this->has_forced_deactivation ) {
429
				add_action( 'switch_theme', array( $this, 'force_deactivation' ) );
430
			}
431
		}
432
433
		/**
434
		 * Hook in plugin action link filters for the WP native plugins page.
435
		 *
436
		 * - Prevent activation of plugins which don't meet the minimum version requirements.
437
		 * - Prevent deactivation of force-activated plugins.
438
		 * - Add update notice if update available.
439
		 *
440
		 * @since 2.5.0
441
		 */
442
		public function add_plugin_action_link_filters() {
443
			foreach ( $this->plugins as $slug => $plugin ) {
444
				if ( false === $this->can_plugin_activate( $slug ) ) {
445
					add_filter( 'plugin_action_links_' . $plugin['file_path'], array( $this, 'filter_plugin_action_links_activate' ), 20 );
446
				}
447
448
				if ( true === $plugin['force_activation'] ) {
449
					add_filter( 'plugin_action_links_' . $plugin['file_path'], array( $this, 'filter_plugin_action_links_deactivate' ), 20 );
450
				}
451
452
				if ( false !== $this->does_plugin_require_update( $slug ) ) {
453
					add_filter( 'plugin_action_links_' . $plugin['file_path'], array( $this, 'filter_plugin_action_links_update' ), 20 );
454
				}
455
			}
456
		}
457
458
		/**
459
		 * Remove the 'Activate' link on the WP native plugins page if the plugin does not meet the
460
		 * minimum version requirements.
461
		 *
462
		 * @since 2.5.0
463
		 *
464
		 * @param array $actions Action links.
465
		 * @return array
466
		 */
467
		public function filter_plugin_action_links_activate( $actions ) {
468
			unset( $actions['activate'] );
469
470
			return $actions;
471
		}
472
473
		/**
474
		 * Remove the 'Deactivate' link on the WP native plugins page if the plugin has been set to force activate.
475
		 *
476
		 * @since 2.5.0
477
		 *
478
		 * @param array $actions Action links.
479
		 * @return array
480
		 */
481
		public function filter_plugin_action_links_deactivate( $actions ) {
482
			unset( $actions['deactivate'] );
483
484
			return $actions;
485
		}
486
487
		/**
488
		 * Add a 'Requires update' link on the WP native plugins page if the plugin does not meet the
489
		 * minimum version requirements.
490
		 *
491
		 * @since 2.5.0
492
		 *
493
		 * @param array $actions Action links.
494
		 * @return array
495
		 */
496
		public function filter_plugin_action_links_update( $actions ) {
497
			$actions['update'] = sprintf(
498
				'<a href="%1$s" title="%2$s" class="edit">%3$s</a>',
499
				esc_url( $this->get_tgmpa_status_url( 'update' ) ),
500
				esc_attr__( 'This plugin needs to be updated to be compatible with your theme.', 'tgmpa' ),
501
				esc_html__( 'Update Required', 'tgmpa' )
502
			);
503
504
			return $actions;
505
		}
506
507
		/**
508
		 * Handles calls to show plugin information via links in the notices.
509
		 *
510
		 * We get the links in the admin notices to point to the TGMPA page, rather
511
		 * than the typical plugin-install.php file, so we can prepare everything
512
		 * beforehand.
513
		 *
514
		 * WP does not make it easy to show the plugin information in the thickbox -
515
		 * here we have to require a file that includes a function that does the
516
		 * main work of displaying it, enqueue some styles, set up some globals and
517
		 * finally call that function before exiting.
518
		 *
519
		 * Down right easy once you know how...
520
		 *
521
		 * Returns early if not the TGMPA page.
522
		 *
523
		 * @since 2.1.0
524
		 *
525
		 * @global string $tab Used as iframe div class names, helps with styling
526
		 * @global string $body_id Used as the iframe body ID, helps with styling
527
		 *
528
		 * @return null Returns early if not the TGMPA page.
529
		 */
530
		public function admin_init() {
531
			if ( ! $this->is_tgmpa_page() ) {
532
				return;
533
			}
534
535
			if ( isset( $_REQUEST['tab'] ) && 'plugin-information' === $_REQUEST['tab'] ) {
536
				// Needed for install_plugin_information().
537
				require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
538
539
				wp_enqueue_style( 'plugin-install' );
540
541
				global $tab, $body_id;
542
				$body_id = 'plugin-information';
543
				// @codingStandardsIgnoreStart
544
				$tab     = 'plugin-information';
545
				// @codingStandardsIgnoreEnd
546
547
				install_plugin_information();
548
549
				exit;
550
			}
551
		}
552
553
		/**
554
		 * Enqueue thickbox scripts/styles for plugin info.
555
		 *
556
		 * Thickbox is not automatically included on all admin pages, so we must
557
		 * manually enqueue it for those pages.
558
		 *
559
		 * Thickbox is only loaded if the user has not dismissed the admin
560
		 * notice or if there are any plugins left to install and activate.
561
		 *
562
		 * @since 2.1.0
563
		 */
564
		public function thickbox() {
565
			if ( ! get_user_meta( get_current_user_id(), 'tgmpa_dismissed_notice_' . $this->id, true ) ) {
566
				add_thickbox();
567
			}
568
		}
569
570
		/**
571
		 * Adds submenu page if there are plugin actions to take.
572
		 *
573
		 * This method adds the submenu page letting users know that a required
574
		 * plugin needs to be installed.
575
		 *
576
		 * This page disappears once the plugin has been installed and activated.
577
		 *
578
		 * @since 1.0.0
579
		 *
580
		 * @see TGM_Plugin_Activation::init()
581
		 * @see TGM_Plugin_Activation::install_plugins_page()
582
		 *
583
		 * @return null Return early if user lacks capability to install a plugin.
584
		 */
585
		public function admin_menu() {
586
			// Make sure privileges are correct to see the page.
587
			if ( ! current_user_can( 'install_plugins' ) ) {
588
				return;
589
			}
590
591
			$args = apply_filters(
592
				'tgmpa_admin_menu_args',
593
				array(
594
					'parent_slug' => $this->parent_slug,                     // Parent Menu slug.
595
					'page_title'  => $this->strings['page_title'],           // Page title.
596
					'menu_title'  => $this->strings['menu_title'],           // Menu title.
597
					'capability'  => $this->capability,                      // Capability.
598
					'menu_slug'   => $this->menu,                            // Menu slug.
599
					'function'    => array( $this, 'install_plugins_page' ), // Callback.
600
				)
601
			);
602
603
			$this->add_admin_menu( $args );
604
		}
605
606
		/**
607
		 * Add the menu item.
608
		 *
609
		 * {@internal IMPORTANT! If this function changes, review the regex in the custom TGMPA
610
		 * generator on the website.}}
611
		 *
612
		 * @since 2.5.0
613
		 *
614
		 * @param array $args Menu item configuration.
615
		 */
616
		protected function add_admin_menu( array $args ) {
617
			if ( has_filter( 'tgmpa_admin_menu_use_add_theme_page' ) ) {
618
				_deprecated_function( 'The "tgmpa_admin_menu_use_add_theme_page" filter', '2.5.0', esc_html__( 'Set the parent_slug config variable instead.', 'tgmpa' ) );
619
			}
620
621
			if ( 'themes.php' === $this->parent_slug ) {
622
				$this->page_hook = call_user_func( 'add_theme_page', $args['page_title'], $args['menu_title'], $args['capability'], $args['menu_slug'], $args['function'] );
623
			} else {
624
				$this->page_hook = call_user_func( 'add_submenu_page', $args['parent_slug'], $args['page_title'], $args['menu_title'], $args['capability'], $args['menu_slug'], $args['function'] );
625
			}
626
		}
627
628
		/**
629
		 * Echoes plugin installation form.
630
		 *
631
		 * This method is the callback for the admin_menu method function.
632
		 * This displays the admin page and form area where the user can select to install and activate the plugin.
633
		 * Aborts early if we're processing a plugin installation action.
634
		 *
635
		 * @since 1.0.0
636
		 *
637
		 * @return null Aborts early if we're processing a plugin installation action.
638
		 */
639
		public function install_plugins_page() {
640
			// Store new instance of plugin table in object.
641
			$plugin_table = new TGMPA_List_Table;
642
643
			// Return early if processing a plugin installation action.
644
			if ( ( ( 'tgmpa-bulk-install' === $plugin_table->current_action() || 'tgmpa-bulk-update' === $plugin_table->current_action() ) && $plugin_table->process_bulk_actions() ) || $this->do_plugin_install() ) {
645
				return;
646
			}
647
648
			// Force refresh of available plugin information so we'll know about manual updates/deletes.
649
			wp_clean_plugins_cache( false );
650
651
			?>
652
			<div class="tgmpa wrap">
653
				<h1><?php echo esc_html( get_admin_page_title() ); ?></h1>
654
				<?php $plugin_table->prepare_items(); ?>
655
656
				<?php
657
				if ( ! empty( $this->message ) && is_string( $this->message ) ) {
658
					echo wp_kses_post( $this->message );
659
				}
660
				?>
661
				<?php $plugin_table->views(); ?>
662
663
				<form id="tgmpa-plugins" action="" method="post">
664
					<input type="hidden" name="tgmpa-page" value="<?php echo esc_attr( $this->menu ); ?>" />
665
					<input type="hidden" name="plugin_status" value="<?php echo esc_attr( $plugin_table->view_context ); ?>" />
666
					<?php $plugin_table->display(); ?>
667
				</form>
668
			</div>
669
			<?php
670
		}
671
672
		/**
673
		 * Installs, updates or activates a plugin depending on the action link clicked by the user.
674
		 *
675
		 * Checks the $_GET variable to see which actions have been
676
		 * passed and responds with the appropriate method.
677
		 *
678
		 * Uses WP_Filesystem to process and handle the plugin installation
679
		 * method.
680
		 *
681
		 * @since 1.0.0
682
		 *
683
		 * @uses WP_Filesystem
684
		 * @uses WP_Error
685
		 * @uses WP_Upgrader
686
		 * @uses Plugin_Upgrader
687
		 * @uses Plugin_Installer_Skin
688
		 * @uses Plugin_Upgrader_Skin
689
		 *
690
		 * @return boolean True on success, false on failure.
691
		 */
692
		protected function do_plugin_install() {
693
			if ( empty( $_GET['plugin'] ) ) {
694
				return false;
695
			}
696
697
			// All plugin information will be stored in an array for processing.
698
			$slug = $this->sanitize_key( urldecode( $_GET['plugin'] ) );
699
700
			if ( ! isset( $this->plugins[ $slug ] ) ) {
701
				return false;
702
			}
703
704
			// Was an install or upgrade action link clicked?
705
			if ( ( isset( $_GET['tgmpa-install'] ) && 'install-plugin' === $_GET['tgmpa-install'] ) || ( isset( $_GET['tgmpa-update'] ) && 'update-plugin' === $_GET['tgmpa-update'] ) ) {
706
707
				$install_type = 'install';
708
				if ( isset( $_GET['tgmpa-update'] ) && 'update-plugin' === $_GET['tgmpa-update'] ) {
709
					$install_type = 'update';
710
				}
711
712
				check_admin_referer( 'tgmpa-' . $install_type, 'tgmpa-nonce' );
713
714
				// Pass necessary information via URL if WP_Filesystem is needed.
715
				$url = wp_nonce_url(
716
					add_query_arg(
717
						array(
718
							'plugin'                 => urlencode( $slug ),
719
							'tgmpa-' . $install_type => $install_type . '-plugin',
720
						),
721
						$this->get_tgmpa_url()
722
					),
723
					'tgmpa-' . $install_type,
724
					'tgmpa-nonce'
725
				);
726
727
				$method = ''; // Leave blank so WP_Filesystem can populate it as necessary.
728
729
				if ( false === ( $creds = request_filesystem_credentials( esc_url_raw( $url ), $method, false, false, array() ) ) ) {
730
					return true;
731
				}
732
733 View Code Duplication
				if ( ! WP_Filesystem( $creds ) ) {
734
					request_filesystem_credentials( esc_url_raw( $url ), $method, true, false, array() ); // Setup WP_Filesystem.
735
					return true;
736
				}
737
738
				/* If we arrive here, we have the filesystem. */
739
740
				// Prep variables for Plugin_Installer_Skin class.
741
				$extra         = array();
742
				$extra['slug'] = $slug; // Needed for potentially renaming of directory name.
743
				$source        = $this->get_download_url( $slug );
744
				$api           = ( 'repo' === $this->plugins[ $slug ]['source_type'] ) ? $this->get_plugins_api( $slug ) : null;
745
				$api           = ( false !== $api ) ? $api : null;
746
747
				$url = add_query_arg(
748
					array(
749
						'action' => $install_type . '-plugin',
750
						'plugin' => urlencode( $slug ),
751
					),
752
					'update.php'
753
				);
754
755
				if ( ! class_exists( 'Plugin_Upgrader', false ) ) {
756
					require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
757
				}
758
759
				$title     = ( 'update' === $install_type ) ? $this->strings['updating'] : $this->strings['installing'];
760
				$skin_args = array(
761
					'type'   => ( 'bundled' !== $this->plugins[ $slug ]['source_type'] ) ? 'web' : 'upload',
762
					'title'  => sprintf( $title, $this->plugins[ $slug ]['name'] ),
763
					'url'    => esc_url_raw( $url ),
764
					'nonce'  => $install_type . '-plugin_' . $slug,
765
					'plugin' => '',
766
					'api'    => $api,
767
					'extra'  => $extra,
768
				);
769
				unset( $title );
770
771
				if ( 'update' === $install_type ) {
772
					$skin_args['plugin'] = $this->plugins[ $slug ]['file_path'];
773
					$skin                = new Plugin_Upgrader_Skin( $skin_args );
774
				} else {
775
					$skin = new Plugin_Installer_Skin( $skin_args );
776
				}
777
778
				// Create a new instance of Plugin_Upgrader.
779
				$upgrader = new Plugin_Upgrader( $skin );
780
781
				// Perform the action and install the plugin from the $source urldecode().
782
				add_filter( 'upgrader_source_selection', array( $this, 'maybe_adjust_source_dir' ), 1, 3 );
783
784
				if ( 'update' === $install_type ) {
785
					// Inject our info into the update transient.
786
					$to_inject                    = array( $slug => $this->plugins[ $slug ] );
787
					$to_inject[ $slug ]['source'] = $source;
788
					$this->inject_update_info( $to_inject );
789
790
					$upgrader->upgrade( $this->plugins[ $slug ]['file_path'] );
791
				} else {
792
					$upgrader->install( $source );
793
				}
794
795
				remove_filter( 'upgrader_source_selection', array( $this, 'maybe_adjust_source_dir' ), 1, 3 );
796
797
				// Make sure we have the correct file path now the plugin is installed/updated.
798
				$this->populate_file_path( $slug );
799
800
				// Only activate plugins if the config option is set to true and the plugin isn't
801
				// already active (upgrade).
802
				if ( $this->is_automatic && ! $this->is_plugin_active( $slug ) ) {
803
					$plugin_activate = $upgrader->plugin_info(); // Grab the plugin info from the Plugin_Upgrader method.
804
					if ( false === $this->activate_single_plugin( $plugin_activate, $slug, true ) ) {
805
						return true; // Finish execution of the function early as we encountered an error.
806
					}
807
				}
808
809
				$this->show_tgmpa_version();
810
811
				// Display message based on if all plugins are now active or not.
812
				if ( $this->is_tgmpa_complete() ) {
813
					echo '<p>', sprintf( esc_html( $this->strings['complete'] ), '<a href="' . esc_url( self_admin_url() ) . '">' . esc_html__( 'Return to the Dashboard', 'tgmpa' ) . '</a>' ), '</p>';
814
					echo '<style type="text/css">#adminmenu .wp-submenu li.current { display: none !important; }</style>';
815
				} else {
816
					echo '<p><a href="', esc_url( $this->get_tgmpa_url() ), '" target="_parent">', esc_html( $this->strings['return'] ), '</a></p>';
817
				}
818
819
				return true;
820
			} elseif ( isset( $this->plugins[ $slug ]['file_path'], $_GET['tgmpa-activate'] ) && 'activate-plugin' === $_GET['tgmpa-activate'] ) {
821
				// Activate action link was clicked.
822
				check_admin_referer( 'tgmpa-activate', 'tgmpa-nonce' );
823
824
				if ( false === $this->activate_single_plugin( $this->plugins[ $slug ]['file_path'], $slug ) ) {
825
					return true; // Finish execution of the function early as we encountered an error.
826
				}
827
			}
828
829
			return false;
830
		}
831
832
		/**
833
		 * Inject information into the 'update_plugins' site transient as WP checks that before running an update.
834
		 *
835
		 * @since 2.5.0
836
		 *
837
		 * @param array $plugins The plugin information for the plugins which are to be updated.
838
		 */
839
		public function inject_update_info( $plugins ) {
840
			$repo_updates = get_site_transient( 'update_plugins' );
841
842
			if ( ! is_object( $repo_updates ) ) {
843
				$repo_updates = new stdClass;
844
			}
845
846
			foreach ( $plugins as $slug => $plugin ) {
847
				$file_path = $plugin['file_path'];
848
849
				if ( empty( $repo_updates->response[ $file_path ] ) ) {
850
					$repo_updates->response[ $file_path ] = new stdClass;
851
				}
852
853
				// We only really need to set package, but let's do all we can in case WP changes something.
854
				$repo_updates->response[ $file_path ]->slug        = $slug;
855
				$repo_updates->response[ $file_path ]->plugin      = $file_path;
856
				$repo_updates->response[ $file_path ]->new_version = $plugin['version'];
857
				$repo_updates->response[ $file_path ]->package     = $plugin['source'];
858
				if ( empty( $repo_updates->response[ $file_path ]->url ) && ! empty( $plugin['external_url'] ) ) {
859
					$repo_updates->response[ $file_path ]->url = $plugin['external_url'];
860
				}
861
			}
862
863
			set_site_transient( 'update_plugins', $repo_updates );
864
		}
865
866
		/**
867
		 * Adjust the plugin directory name if necessary.
868
		 *
869
		 * The final destination directory of a plugin is based on the subdirectory name found in the
870
		 * (un)zipped source. In some cases - most notably GitHub repository plugin downloads -, this
871
		 * subdirectory name is not the same as the expected slug and the plugin will not be recognized
872
		 * as installed. This is fixed by adjusting the temporary unzipped source subdirectory name to
873
		 * the expected plugin slug.
874
		 *
875
		 * @since 2.5.0
876
		 *
877
		 * @param string       $source        Path to upgrade/zip-file-name.tmp/subdirectory/.
878
		 * @param string       $remote_source Path to upgrade/zip-file-name.tmp.
879
		 * @param \WP_Upgrader $upgrader      Instance of the upgrader which installs the plugin.
880
		 * @return string $source
881
		 */
882
		public function maybe_adjust_source_dir( $source, $remote_source, $upgrader ) {
883
			if ( ! $this->is_tgmpa_page() || ! is_object( $GLOBALS['wp_filesystem'] ) ) {
884
				return $source;
885
			}
886
887
			// Check for single file plugins.
888
			$source_files = array_keys( $GLOBALS['wp_filesystem']->dirlist( $remote_source ) );
889
			if ( 1 === count( $source_files ) && false === $GLOBALS['wp_filesystem']->is_dir( $source ) ) {
890
				return $source;
891
			}
892
893
			// Multi-file plugin, let's see if the directory is correctly named.
894
			$desired_slug = '';
895
896
			// Figure out what the slug is supposed to be.
897
			if ( false === $upgrader->bulk && ! empty( $upgrader->skin->options['extra']['slug'] ) ) {
898
				$desired_slug = $upgrader->skin->options['extra']['slug'];
899
			} else {
900
				// Bulk installer contains less info, so fall back on the info registered here.
901
				foreach ( $this->plugins as $slug => $plugin ) {
902
					if ( ! empty( $upgrader->skin->plugin_names[ $upgrader->skin->i ] ) && $plugin['name'] === $upgrader->skin->plugin_names[ $upgrader->skin->i ] ) {
903
						$desired_slug = $slug;
904
						break;
905
					}
906
				}
907
				unset( $slug, $plugin );
908
			}
909
910
			if ( ! empty( $desired_slug ) ) {
911
				$subdir_name = untrailingslashit( str_replace( trailingslashit( $remote_source ), '', $source ) );
912
913
				if ( ! empty( $subdir_name ) && $subdir_name !== $desired_slug ) {
914
					$from = untrailingslashit( $source );
915
					$to   = trailingslashit( $remote_source ) . $desired_slug;
916
917
					if ( true === $GLOBALS['wp_filesystem']->move( $from, $to ) ) {
918
						return trailingslashit( $to );
919 View Code Duplication
					} else {
920
						return new WP_Error( 'rename_failed', esc_html__( 'The remote plugin package does not contain a folder with the desired slug and renaming did not work.', 'tgmpa' ) . ' ' . esc_html__( 'Please contact the plugin provider and ask them to package their plugin according to the WordPress guidelines.', 'tgmpa' ), array( 'found' => $subdir_name, 'expected' => $desired_slug ) );
921
					}
922 View Code Duplication
				} elseif ( empty( $subdir_name ) ) {
923
					return new WP_Error( 'packaged_wrong', esc_html__( 'The remote plugin package consists of more than one file, but the files are not packaged in a folder.', 'tgmpa' ) . ' ' . esc_html__( 'Please contact the plugin provider and ask them to package their plugin according to the WordPress guidelines.', 'tgmpa' ), array( 'found' => $subdir_name, 'expected' => $desired_slug ) );
924
				}
925
			}
926
927
			return $source;
928
		}
929
930
		/**
931
		 * Activate a single plugin and send feedback about the result to the screen.
932
		 *
933
		 * @since 2.5.0
934
		 *
935
		 * @param string $file_path Path within wp-plugins/ to main plugin file.
936
		 * @param string $slug      Plugin slug.
937
		 * @param bool   $automatic Whether this is an automatic activation after an install. Defaults to false.
938
		 *                          This determines the styling of the output messages.
939
		 * @return bool False if an error was encountered, true otherwise.
940
		 */
941
		protected function activate_single_plugin( $file_path, $slug, $automatic = false ) {
942
			if ( $this->can_plugin_activate( $slug ) ) {
943
				$activate = activate_plugin( $file_path );
944
945
				if ( is_wp_error( $activate ) ) {
946
					echo '<div id="message" class="error"><p>', wp_kses_post( $activate->get_error_message() ), '</p></div>',
947
						'<p><a href="', esc_url( $this->get_tgmpa_url() ), '" target="_parent">', esc_html( $this->strings['return'] ), '</a></p>';
948
949
					return false; // End it here if there is an error with activation.
950
				} else {
951
					if ( ! $automatic ) {
952
						// Make sure message doesn't display again if bulk activation is performed
953
						// immediately after a single activation.
954
						if ( ! isset( $_POST['action'] ) ) { // WPCS: CSRF OK.
955
							echo '<div id="message" class="updated"><p>', esc_html( $this->strings['activated_successfully'] ), ' <strong>', esc_html( $this->plugins[ $slug ]['name'] ), '.</strong></p></div>';
956
						}
957
					} else {
958
						// Simpler message layout for use on the plugin install page.
959
						echo '<p>', esc_html( $this->strings['plugin_activated'] ), '</p>';
960
					}
961
				}
962 View Code Duplication
			} elseif ( $this->is_plugin_active( $slug ) ) {
963
				// No simpler message format provided as this message should never be encountered
964
				// on the plugin install page.
965
				echo '<div id="message" class="error"><p>',
966
					sprintf(
967
						esc_html( $this->strings['plugin_already_active'] ),
968
						'<strong>' . esc_html( $this->plugins[ $slug ]['name'] ) . '</strong>'
969
					),
970
					'</p></div>';
971
			} elseif ( $this->does_plugin_require_update( $slug ) ) {
972
				if ( ! $automatic ) {
973
					// Make sure message doesn't display again if bulk activation is performed
974
					// immediately after a single activation.
975 View Code Duplication
					if ( ! isset( $_POST['action'] ) ) { // WPCS: CSRF OK.
976
						echo '<div id="message" class="error"><p>',
977
							sprintf(
978
								esc_html( $this->strings['plugin_needs_higher_version'] ),
979
								'<strong>' . esc_html( $this->plugins[ $slug ]['name'] ) . '</strong>'
980
							),
981
							'</p></div>';
982
					}
983
				} else {
984
					// Simpler message layout for use on the plugin install page.
985
					echo '<p>', sprintf( esc_html( $this->strings['plugin_needs_higher_version'] ), esc_html( $this->plugins[ $slug ]['name'] ) ), '</p>';
986
				}
987
			}
988
989
			return true;
990
		}
991
992
		/**
993
		 * Echoes required plugin notice.
994
		 *
995
		 * Outputs a message telling users that a specific plugin is required for
996
		 * their theme. If appropriate, it includes a link to the form page where
997
		 * users can install and activate the plugin.
998
		 *
999
		 * Returns early if we're on the Install page.
1000
		 *
1001
		 * @since 1.0.0
1002
		 *
1003
		 * @global object $current_screen
1004
		 *
1005
		 * @return null Returns early if we're on the Install page.
1006
		 */
1007
		public function notices() {
1008
			// Remove nag on the install page / Return early if the nag message has been dismissed or user < author.
1009
			if ( $this->is_tgmpa_page() || get_user_meta( get_current_user_id(), 'tgmpa_dismissed_notice_' . $this->id, true ) || ! current_user_can( apply_filters( 'tgmpa_show_admin_notice_capability', 'publish_posts' ) ) ) {
1010
				return;
1011
			}
1012
1013
			// Store for the plugin slugs by message type.
1014
			$message = array();
1015
1016
			// Initialize counters used to determine plurality of action link texts.
1017
			$install_link_count          = 0;
1018
			$update_link_count           = 0;
1019
			$activate_link_count         = 0;
1020
			$total_required_action_count = 0;
1021
1022
			foreach ( $this->plugins as $slug => $plugin ) {
1023
				if ( $this->is_plugin_active( $slug ) && false === $this->does_plugin_have_update( $slug ) ) {
1024
					continue;
1025
				}
1026
1027
				if ( ! $this->is_plugin_installed( $slug ) ) {
1028
					if ( current_user_can( 'install_plugins' ) ) {
1029
						$install_link_count++;
1030
1031
						if ( true === $plugin['required'] ) {
1032
							$message['notice_can_install_required'][] = $slug;
1033
						} else {
1034
							$message['notice_can_install_recommended'][] = $slug;
1035
						}
1036
					}
1037
					if ( true === $plugin['required'] ) {
1038
						$total_required_action_count++;
1039
					}
1040
				} else {
1041
					if ( ! $this->is_plugin_active( $slug ) && $this->can_plugin_activate( $slug ) ) {
1042
						if ( current_user_can( 'activate_plugins' ) ) {
1043
							$activate_link_count++;
1044
1045
							if ( true === $plugin['required'] ) {
1046
								$message['notice_can_activate_required'][] = $slug;
1047
							} else {
1048
								$message['notice_can_activate_recommended'][] = $slug;
1049
							}
1050
						}
1051
						if ( true === $plugin['required'] ) {
1052
							$total_required_action_count++;
1053
						}
1054
					}
1055
1056
					if ( $this->does_plugin_require_update( $slug ) || false !== $this->does_plugin_have_update( $slug ) ) {
1057
1058
						if ( current_user_can( 'update_plugins' ) ) {
1059
							$update_link_count++;
1060
1061
							if ( $this->does_plugin_require_update( $slug ) ) {
1062
								$message['notice_ask_to_update'][] = $slug;
1063
							} elseif ( false !== $this->does_plugin_have_update( $slug ) ) {
1064
								$message['notice_ask_to_update_maybe'][] = $slug;
1065
							}
1066
						}
1067
						if ( true === $plugin['required'] ) {
1068
							$total_required_action_count++;
1069
						}
1070
					}
1071
				}
1072
			}
1073
			unset( $slug, $plugin );
1074
1075
			// If we have notices to display, we move forward.
1076
			if ( ! empty( $message ) || $total_required_action_count > 0 ) {
1077
				krsort( $message ); // Sort messages.
1078
				$rendered = '';
1079
1080
				// As add_settings_error() wraps the final message in a <p> and as the final message can't be
1081
				// filtered, using <p>'s in our html would render invalid html output.
1082
				$line_template = '<span style="display: block; margin: 0.5em 0.5em 0 0; clear: both;">%s</span>' . "\n";
1083
1084
				if ( ! current_user_can( 'activate_plugins' ) && ! current_user_can( 'install_plugins' ) && ! current_user_can( 'update_plugins' ) ) {
1085
					$rendered = esc_html__( $this->strings['notice_cannot_install_activate'] ) . ' ' . esc_html__( $this->strings['contact_admin'] );
1086
					$rendered .= $this->create_user_action_links_for_notice( 0, 0, 0, $line_template );
1087
				} else {
1088
1089
					// If dismissable is false and a message is set, output it now.
1090
					if ( ! $this->dismissable && ! empty( $this->dismiss_msg ) ) {
1091
						$rendered .= sprintf( $line_template, wp_kses_post( $this->dismiss_msg ) );
1092
					}
1093
1094
					// Render the individual message lines for the notice.
1095
					foreach ( $message as $type => $plugin_group ) {
1096
						$linked_plugins = array();
1097
1098
						// Get the external info link for a plugin if one is available.
1099
						foreach ( $plugin_group as $plugin_slug ) {
1100
							$linked_plugins[] = $this->get_info_link( $plugin_slug );
1101
						}
1102
						unset( $plugin_slug );
1103
1104
						$count          = count( $plugin_group );
1105
						$linked_plugins = array_map( array( 'TGMPA_Utils', 'wrap_in_em' ), $linked_plugins );
1106
						$last_plugin    = array_pop( $linked_plugins ); // Pop off last name to prep for readability.
1107
						$imploded       = empty( $linked_plugins ) ? $last_plugin : ( implode( ', ', $linked_plugins ) . ' ' . esc_html_x( 'and', 'plugin A *and* plugin B', 'tgmpa' ) . ' ' . $last_plugin );
1108
1109
						$rendered .= sprintf(
1110
							$line_template,
1111
							sprintf(
1112
								translate_nooped_plural( $this->strings[ $type ], $count, 'tgmpa' ),
1113
								$imploded,
1114
								$count
1115
							)
1116
						);
1117
1118
					}
1119
					unset( $type, $plugin_group, $linked_plugins, $count, $last_plugin, $imploded );
1120
1121
					$rendered .= $this->create_user_action_links_for_notice( $install_link_count, $update_link_count, $activate_link_count, $line_template );
1122
				}
1123
1124
				// Register the nag messages and prepare them to be processed.
1125
				add_settings_error( 'tgmpa', 'tgmpa', $rendered, $this->get_admin_notice_class() );
1126
			}
1127
1128
			// Admin options pages already output settings_errors, so this is to avoid duplication.
1129
			if ( 'options-general' !== $GLOBALS['current_screen']->parent_base ) {
1130
				$this->display_settings_errors();
1131
			}
1132
		}
1133
1134
		/**
1135
		 * Generate the user action links for the admin notice.
1136
		 *
1137
		 * @since 2.x.x
1138
		 *
1139
		 * @param int $install_count  Number of plugins to install.
1140
		 * @param int $update_count   Number of plugins to update.
1141
		 * @param int $activate_count Number of plugins to activate.
1142
		 * @param int $line_template  Template for the HTML tag to output a line.
1143
		 * @return string Action links.
1144
		 */
1145
		protected function create_user_action_links_for_notice( $install_count, $update_count, $activate_count, $line_template ) {
1146
			// Setup action links.
1147
			$action_links = array(
1148
				'install'  => '',
1149
				'update'   => '',
1150
				'activate' => '',
1151
				'dismiss'  => $this->dismissable ? '<a href="' . esc_url( wp_nonce_url( add_query_arg( 'tgmpa-dismiss', 'dismiss_admin_notices' ), 'tgmpa-dismiss-' . get_current_user_id() ) ) . '" class="dismiss-notice" target="_parent">' . esc_html( $this->strings['dismiss'] ) . '</a>' : '',
1152
			);
1153
1154
			$link_template = '<a href="%2$s">%1$s</a>';
1155
1156
			if ( current_user_can( 'install_plugins' ) ) {
1157 View Code Duplication
				if ( $install_count > 0 ) {
1158
					$action_links['install'] = sprintf(
1159
						$link_template,
1160
						translate_nooped_plural( $this->strings['install_link'], $install_count, 'tgmpa' ),
1161
						esc_url( $this->get_tgmpa_status_url( 'install' ) )
1162
					);
1163
				}
1164 View Code Duplication
				if ( $update_count > 0 ) {
1165
					$action_links['update'] = sprintf(
1166
						$link_template,
1167
						translate_nooped_plural( $this->strings['update_link'], $update_count, 'tgmpa' ),
1168
						esc_url( $this->get_tgmpa_status_url( 'update' ) )
1169
					);
1170
				}
1171
			}
1172
1173 View Code Duplication
			if ( current_user_can( 'activate_plugins' ) && $activate_count > 0 ) {
1174
				$action_links['activate'] = sprintf(
1175
					$link_template,
1176
					translate_nooped_plural( $this->strings['activate_link'], $activate_count, 'tgmpa' ),
1177
					esc_url( $this->get_tgmpa_status_url( 'activate' ) )
1178
				);
1179
			}
1180
1181
			$action_links = apply_filters( 'tgmpa_notice_action_links', $action_links );
1182
1183
			$action_links = array_filter( (array) $action_links ); // Remove any empty array items.
1184
1185
			if ( ! empty( $action_links ) ) {
1186
				$action_links = sprintf( $line_template, implode( ' | ', $action_links ) );
1187
				return apply_filters( 'tgmpa_notice_rendered_action_links', $action_links );
1188
			} else {
1189
				return '';
1190
			}
1191
		}
1192
1193
		/**
1194
		 * Get admin notice class.
1195
		 *
1196
		 * Work around all the changes to the various admin notice classes between WP 4.4 and 3.7
1197
		 * (lowest supported version by TGMPA).
1198
		 *
1199
		 * @since 2.x.x
1200
		 *
1201
		 * @return string
1202
		 */
1203
		protected function get_admin_notice_class() {
1204
			if ( ! empty( $this->strings['nag_type'] ) ) {
1205
				return sanitize_html_class( strtolower( $this->strings['nag_type'] ) );
1206
			} else {
1207
				if ( version_compare( $this->wp_version, '4.2', '>=' ) ) {
1208
					return 'notice-warning';
1209
				} elseif ( version_compare( $this->wp_version, '4.1', '>=' ) ) {
1210
					return 'notice';
1211
				} else {
1212
					return 'updated';
1213
				}
1214
			}
1215
		}
1216
1217
		/**
1218
		 * Display settings errors and remove those which have been displayed to avoid duplicate messages showing
1219
		 *
1220
		 * @since 2.5.0
1221
		 */
1222
		protected function display_settings_errors() {
1223
			global $wp_settings_errors;
1224
1225
			settings_errors( 'tgmpa' );
1226
1227
			foreach ( (array) $wp_settings_errors as $key => $details ) {
1228
				if ( 'tgmpa' === $details['setting'] ) {
1229
					unset( $wp_settings_errors[ $key ] );
1230
					break;
1231
				}
1232
			}
1233
		}
1234
1235
		/**
1236
		 * Add dismissable admin notices.
1237
		 *
1238
		 * Appends a link to the admin nag messages. If clicked, the admin notice disappears and no longer is visible to users.
1239
		 *
1240
		 * @since 2.1.0
1241
		 */
1242
		public function dismiss() {
1243
			if ( isset( $_GET['tgmpa-dismiss'] ) && check_admin_referer( 'tgmpa-dismis-' . get_current_user_id() ) ) {
1244
				update_user_meta( get_current_user_id(), 'tgmpa_dismissed_notice_' . $this->id, 1 );
1245
			}
1246
		}
1247
1248
		/**
1249
		 * Add individual plugin to our collection of plugins.
1250
		 *
1251
		 * If the required keys are not set or the plugin has already
1252
		 * been registered, the plugin is not added.
1253
		 *
1254
		 * @since 2.0.0
1255
		 *
1256
		 * @param array|null $plugin Array of plugin arguments or null if invalid argument.
1257
		 * @return null Return early if incorrect argument.
1258
		 */
1259
		public function register( $plugin ) {
1260
			if ( empty( $plugin['slug'] ) || empty( $plugin['name'] ) ) {
1261
				return;
1262
			}
1263
1264
			if ( empty( $plugin['slug'] ) || ! is_string( $plugin['slug'] ) || isset( $this->plugins[ $plugin['slug'] ] ) ) {
1265
				return;
1266
			}
1267
1268
			$defaults = array(
1269
				'name'               => '',      // String
1270
				'slug'               => '',      // String
1271
				'source'             => 'repo',  // String
1272
				'required'           => false,   // Boolean
1273
				'version'            => '',      // String
1274
				'force_activation'   => false,   // Boolean
1275
				'force_deactivation' => false,   // Boolean
1276
				'external_url'       => '',      // String
1277
				'is_callable'        => '',      // String|Array.
1278
			);
1279
1280
			// Prepare the received data.
1281
			$plugin = wp_parse_args( $plugin, $defaults );
1282
1283
			// Standardize the received slug.
1284
			$plugin['slug'] = $this->sanitize_key( $plugin['slug'] );
1285
1286
			// Forgive users for using string versions of booleans or floats for version number.
1287
			$plugin['version']            = (string) $plugin['version'];
1288
			$plugin['source']             = empty( $plugin['source'] ) ? 'repo' : $plugin['source'];
1289
			$plugin['required']           = TGMPA_Utils::validate_bool( $plugin['required'] );
1290
			$plugin['force_activation']   = TGMPA_Utils::validate_bool( $plugin['force_activation'] );
1291
			$plugin['force_deactivation'] = TGMPA_Utils::validate_bool( $plugin['force_deactivation'] );
1292
1293
			// Enrich the received data.
1294
			$plugin['file_path']   = $this->_get_plugin_basename_from_slug( $plugin['slug'] );
1295
			$plugin['source_type'] = $this->get_plugin_source_type( $plugin['source'] );
1296
1297
			// Set the class properties.
1298
			$this->plugins[ $plugin['slug'] ]    = $plugin;
1299
			$this->sort_order[ $plugin['slug'] ] = $plugin['name'];
1300
1301
			// Should we add the force activation hook ?
1302
			if ( true === $plugin['force_activation'] ) {
1303
				$this->has_forced_activation = true;
1304
			}
1305
1306
			// Should we add the force deactivation hook ?
1307
			if ( true === $plugin['force_deactivation'] ) {
1308
				$this->has_forced_deactivation = true;
1309
			}
1310
		}
1311
1312
		/**
1313
		 * Determine what type of source the plugin comes from.
1314
		 *
1315
		 * @since 2.5.0
1316
		 *
1317
		 * @param string $source The source of the plugin as provided, either empty (= WP repo), a file path
1318
		 *                       (= bundled) or an external URL.
1319
		 * @return string 'repo', 'external', or 'bundled'
1320
		 */
1321
		protected function get_plugin_source_type( $source ) {
1322
			if ( 'repo' === $source || preg_match( self::WP_REPO_REGEX, $source ) ) {
1323
				return 'repo';
1324
			} elseif ( preg_match( self::IS_URL_REGEX, $source ) ) {
1325
				return 'external';
1326
			} else {
1327
				return 'bundled';
1328
			}
1329
		}
1330
1331
		/**
1332
		 * Sanitizes a string key.
1333
		 *
1334
		 * Near duplicate of WP Core `sanitize_key()`. The difference is that uppercase characters *are*
1335
		 * allowed, so as not to break upgrade paths from non-standard bundled plugins using uppercase
1336
		 * characters in the plugin directory path/slug. Silly them.
1337
		 *
1338
		 * @see https://developer.wordpress.org/reference/hooks/sanitize_key/
1339
		 *
1340
		 * @since 2.5.0
1341
		 *
1342
		 * @param string $key String key.
1343
		 * @return string Sanitized key
1344
		 */
1345
		public function sanitize_key( $key ) {
1346
			$raw_key = $key;
1347
			$key     = preg_replace( '`[^A-Za-z0-9_-]`', '', $key );
1348
1349
			/**
1350
			* Filter a sanitized key string.
1351
			*
1352
			* @since 3.0.0
1353
			*
1354
			* @param string $key     Sanitized key.
1355
			* @param string $raw_key The key prior to sanitization.
1356
			*/
1357
			return apply_filters( 'tgmpa_sanitize_key', $key, $raw_key );
1358
		}
1359
1360
		/**
1361
		 * Amend default configuration settings.
1362
		 *
1363
		 * @since 2.0.0
1364
		 *
1365
		 * @param array $config Array of config options to pass as class properties.
1366
		 */
1367
		public function config( $config ) {
1368
			$keys = array(
1369
				'id',
1370
				'default_path',
1371
				'has_notices',
1372
				'dismissable',
1373
				'dismiss_msg',
1374
				'menu',
1375
				'parent_slug',
1376
				'capability',
1377
				'is_automatic',
1378
				'message',
1379
				'strings',
1380
			);
1381
1382
			foreach ( $keys as $key ) {
1383
				if ( isset( $config[ $key ] ) ) {
1384
					if ( is_array( $config[ $key ] ) ) {
1385
						$this->$key = array_merge( $this->$key, $config[ $key ] );
1386
					} else {
1387
						$this->$key = $config[ $key ];
1388
					}
1389
				}
1390
			}
1391
		}
1392
1393
		/**
1394
		 * Amend action link after plugin installation.
1395
		 *
1396
		 * @since 2.0.0
1397
		 *
1398
		 * @param array $install_actions Existing array of actions.
1399
		 * @return array Amended array of actions.
1400
		 */
1401
		public function actions( $install_actions ) {
1402
			// Remove action links on the TGMPA install page.
1403
			if ( $this->is_tgmpa_page() ) {
1404
				return false;
1405
			}
1406
1407
			return $install_actions;
1408
		}
1409
1410
		/**
1411
		 * Flushes the plugins cache on theme switch to prevent stale entries
1412
		 * from remaining in the plugin table.
1413
		 *
1414
		 * @since 2.4.0
1415
		 *
1416
		 * @param bool $clear_update_cache Optional. Whether to clear the Plugin updates cache.
1417
		 *                                 Parameter added in v2.5.0.
1418
		 */
1419
		public function flush_plugins_cache( $clear_update_cache = true ) {
1420
			wp_clean_plugins_cache( $clear_update_cache );
1421
		}
1422
1423
		/**
1424
		 * Set file_path key for each installed plugin.
1425
		 *
1426
		 * @since 2.1.0
1427
		 *
1428
		 * @param string $plugin_slug Optional. If set, only (re-)populates the file path for that specific plugin.
1429
		 *                            Parameter added in v2.5.0.
1430
		 */
1431
		public function populate_file_path( $plugin_slug = '' ) {
1432
			if ( ! empty( $plugin_slug ) && is_string( $plugin_slug ) && isset( $this->plugins[ $plugin_slug ] ) ) {
1433
				$this->plugins[ $plugin_slug ]['file_path'] = $this->_get_plugin_basename_from_slug( $plugin_slug );
1434
			} else {
1435
				// Add file_path key for all plugins.
1436
				foreach ( $this->plugins as $slug => $values ) {
1437
					$this->plugins[ $slug ]['file_path'] = $this->_get_plugin_basename_from_slug( $slug );
1438
				}
1439
			}
1440
		}
1441
1442
		/**
1443
		 * Helper function to extract the file path of the plugin file from the
1444
		 * plugin slug, if the plugin is installed.
1445
		 *
1446
		 * @since 2.0.0
1447
		 *
1448
		 * @param string $slug Plugin slug (typically folder name) as provided by the developer.
1449
		 * @return string Either file path for plugin if installed, or just the plugin slug.
1450
		 */
1451
		protected function _get_plugin_basename_from_slug( $slug ) {
1452
			$keys = array_keys( $this->get_plugins() );
1453
1454
			foreach ( $keys as $key ) {
1455
				if ( preg_match( '|^' . $slug . '/|', $key ) ) {
1456
					return $key;
1457
				}
1458
			}
1459
1460
			return $slug;
1461
		}
1462
1463
		/**
1464
		 * Retrieve plugin data, given the plugin name.
1465
		 *
1466
		 * Loops through the registered plugins looking for $name. If it finds it,
1467
		 * it returns the $data from that plugin. Otherwise, returns false.
1468
		 *
1469
		 * @since 2.1.0
1470
		 *
1471
		 * @param string $name Name of the plugin, as it was registered.
1472
		 * @param string $data Optional. Array key of plugin data to return. Default is slug.
1473
		 * @return string|boolean Plugin slug if found, false otherwise.
1474
		 */
1475
		public function _get_plugin_data_from_name( $name, $data = 'slug' ) {
1476
			foreach ( $this->plugins as $values ) {
1477
				if ( $name === $values['name'] && isset( $values[ $data ] ) ) {
1478
					return $values[ $data ];
1479
				}
1480
			}
1481
1482
			return false;
1483
		}
1484
1485
		/**
1486
		 * Retrieve the download URL for a package.
1487
		 *
1488
		 * @since 2.5.0
1489
		 *
1490
		 * @param string $slug Plugin slug.
1491
		 * @return string Plugin download URL or path to local file or empty string if undetermined.
1492
		 */
1493
		public function get_download_url( $slug ) {
1494
			$dl_source = '';
1495
1496
			switch ( $this->plugins[ $slug ]['source_type'] ) {
1497
				case 'repo':
1498
					return $this->get_wp_repo_download_url( $slug );
1499
				case 'external':
1500
					return $this->plugins[ $slug ]['source'];
1501
				case 'bundled':
1502
					return $this->default_path . $this->plugins[ $slug ]['source'];
1503
			}
1504
1505
			return $dl_source; // Should never happen.
1506
		}
1507
1508
		/**
1509
		 * Retrieve the download URL for a WP repo package.
1510
		 *
1511
		 * @since 2.5.0
1512
		 *
1513
		 * @param string $slug Plugin slug.
1514
		 * @return string Plugin download URL.
1515
		 */
1516
		protected function get_wp_repo_download_url( $slug ) {
1517
			$source = '';
1518
			$api    = $this->get_plugins_api( $slug );
1519
1520
			if ( false !== $api && isset( $api->download_link ) ) {
1521
				$source = $api->download_link;
1522
			}
1523
1524
			return $source;
1525
		}
1526
1527
		/**
1528
		 * Try to grab information from WordPress API.
1529
		 *
1530
		 * @since 2.5.0
1531
		 *
1532
		 * @param string $slug Plugin slug.
1533
		 * @return object Plugins_api response object on success, WP_Error on failure.
1534
		 */
1535
		protected function get_plugins_api( $slug ) {
1536
			static $api = array(); // Cache received responses.
1537
1538
			if ( ! isset( $api[ $slug ] ) ) {
1539
				if ( ! function_exists( 'plugins_api' ) ) {
1540
					require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
1541
				}
1542
1543
				$response = plugins_api( 'plugin_information', array( 'slug' => $slug, 'fields' => array( 'sections' => false ) ) );
1544
1545
				$api[ $slug ] = false;
1546
1547
				if ( is_wp_error( $response ) ) {
1548
					wp_die( esc_html( $this->strings['oops'] ) );
1549
				} else {
1550
					$api[ $slug ] = $response;
1551
				}
1552
			}
1553
1554
			return $api[ $slug ];
1555
		}
1556
1557
		/**
1558
		 * Retrieve a link to a plugin information page.
1559
		 *
1560
		 * @since 2.5.0
1561
		 *
1562
		 * @param string $slug Plugin slug.
1563
		 * @return string Fully formed html link to a plugin information page if available
1564
		 *                or the plugin name if not.
1565
		 */
1566
		public function get_info_link( $slug ) {
1567
			if ( ! empty( $this->plugins[ $slug ]['external_url'] ) && preg_match( self::IS_URL_REGEX, $this->plugins[ $slug ]['external_url'] ) ) {
1568
				$link = sprintf(
1569
					'<a href="%1$s" target="_blank">%2$s</a>',
1570
					esc_url( $this->plugins[ $slug ]['external_url'] ),
1571
					esc_html( $this->plugins[ $slug ]['name'] )
1572
				);
1573
			} elseif ( 'repo' === $this->plugins[ $slug ]['source_type'] ) {
1574
				$url = add_query_arg(
1575
					array(
1576
						'tab'       => 'plugin-information',
1577
						'plugin'    => urlencode( $slug ),
1578
						'TB_iframe' => 'true',
1579
						'width'     => '640',
1580
						'height'    => '500',
1581
					),
1582
					self_admin_url( 'plugin-install.php' )
1583
				);
1584
1585
				$link = sprintf(
1586
					'<a href="%1$s" class="thickbox">%2$s</a>',
1587
					esc_url( $url ),
1588
					esc_html( $this->plugins[ $slug ]['name'] )
1589
				);
1590
			} else {
1591
				$link = esc_html( $this->plugins[ $slug ]['name'] ); // No hyperlink.
1592
			}
1593
1594
			return $link;
1595
		}
1596
1597
		/**
1598
		 * Determine if we're on the TGMPA Install page.
1599
		 *
1600
		 * @since 2.1.0
1601
		 *
1602
		 * @return boolean True when on the TGMPA page, false otherwise.
1603
		 */
1604
		protected function is_tgmpa_page() {
1605
			return isset( $_GET['page'] ) && $this->menu === $_GET['page'];
1606
		}
1607
1608
		/**
1609
		 * Retrieve the URL to the TGMPA Install page.
1610
		 *
1611
		 * I.e. depending on the config settings passed something along the lines of:
1612
		 * http://example.com/wp-admin/themes.php?page=tgmpa-install-plugins
1613
		 *
1614
		 * @since 2.5.0
1615
		 *
1616
		 * @return string Properly encoded URL (not escaped).
1617
		 */
1618
		public function get_tgmpa_url() {
1619
			static $url;
1620
1621
			if ( ! isset( $url ) ) {
1622
				$parent = $this->parent_slug;
1623
				if ( false === strpos( $parent, '.php' ) ) {
1624
					$parent = 'admin.php';
1625
				}
1626
				$url = add_query_arg(
1627
					array(
1628
						'page' => urlencode( $this->menu ),
1629
					),
1630
					self_admin_url( $parent )
1631
				);
1632
			}
1633
1634
			return $url;
1635
		}
1636
1637
		/**
1638
		 * Retrieve the URL to the TGMPA Install page for a specific plugin status (view).
1639
		 *
1640
		 * I.e. depending on the config settings passed something along the lines of:
1641
		 * http://example.com/wp-admin/themes.php?page=tgmpa-install-plugins&plugin_status=install
1642
		 *
1643
		 * @since 2.5.0
1644
		 *
1645
		 * @param string $status Plugin status - either 'install', 'update' or 'activate'.
1646
		 * @return string Properly encoded URL (not escaped).
1647
		 */
1648
		public function get_tgmpa_status_url( $status ) {
1649
			return add_query_arg(
1650
				array(
1651
					'plugin_status' => urlencode( $status ),
1652
				),
1653
				$this->get_tgmpa_url()
1654
			);
1655
		}
1656
1657
		/**
1658
		 * Determine whether there are open actions for plugins registered with TGMPA.
1659
		 *
1660
		 * @since 2.5.0
1661
		 *
1662
		 * @return bool True if complete, i.e. no outstanding actions. False otherwise.
1663
		 */
1664
		public function is_tgmpa_complete() {
1665
			$complete = true;
1666
			foreach ( $this->plugins as $slug => $plugin ) {
1667
				if ( ! $this->is_plugin_active( $slug ) || false !== $this->does_plugin_have_update( $slug ) ) {
1668
					$complete = false;
1669
					break;
1670
				}
1671
			}
1672
1673
			return $complete;
1674
		}
1675
1676
		/**
1677
		 * Check if a plugin is installed. Does not take must-use plugins into account.
1678
		 *
1679
		 * @since 2.5.0
1680
		 *
1681
		 * @param string $slug Plugin slug.
1682
		 * @return bool True if installed, false otherwise.
1683
		 */
1684
		public function is_plugin_installed( $slug ) {
1685
			$installed_plugins = $this->get_plugins(); // Retrieve a list of all installed plugins (WP cached).
1686
1687
			return ( ! empty( $installed_plugins[ $this->plugins[ $slug ]['file_path'] ] ) );
1688
		}
1689
1690
		/**
1691
		 * Check if a plugin is active.
1692
		 *
1693
		 * @since 2.5.0
1694
		 *
1695
		 * @param string $slug Plugin slug.
1696
		 * @return bool True if active, false otherwise.
1697
		 */
1698
		public function is_plugin_active( $slug ) {
1699
			return ( ( ! empty( $this->plugins[ $slug ]['is_callable'] ) && is_callable( $this->plugins[ $slug ]['is_callable'] ) ) || is_plugin_active( $this->plugins[ $slug ]['file_path'] ) );
1700
		}
1701
1702
		/**
1703
		 * Check if a plugin can be updated, i.e. if we have information on the minimum WP version required
1704
		 * available, check whether the current install meets them.
1705
		 *
1706
		 * @since 2.5.0
1707
		 *
1708
		 * @param string $slug Plugin slug.
1709
		 * @return bool True if OK to update, false otherwise.
1710
		 */
1711
		public function can_plugin_update( $slug ) {
1712
			// We currently can't get reliable info on non-WP-repo plugins - issue #380.
1713
			if ( 'repo' !== $this->plugins[ $slug ]['source_type'] ) {
1714
				return true;
1715
			}
1716
1717
			$api = $this->get_plugins_api( $slug );
1718
1719
			if ( false !== $api && isset( $api->requires ) ) {
1720
				return version_compare( $this->wp_version, $api->requires, '>=' );
1721
			}
1722
1723
			// No usable info received from the plugins API, presume we can update.
1724
			return true;
1725
		}
1726
1727
		/**
1728
		 * Check to see if the plugin is 'updatetable', i.e. installed, with an update available
1729
		 * and no WP version requirements blocking it.
1730
		 *
1731
		 * @since 2.x.x
1732
		 *
1733
		 * @param string $slug Plugin slug.
1734
		 * @return bool True if OK to proceed with update, false otherwise.
1735
		 */
1736
		public function is_plugin_updatetable( $slug ) {
1737
			if ( ! $this->is_plugin_installed( $slug ) ) {
1738
				return false;
1739
			} else {
1740
				return ( $this->does_plugin_have_update( $slug ) && $this->can_plugin_update( $slug ) );
1741
			}
1742
		}
1743
1744
		/**
1745
		 * Check if a plugin can be activated, i.e. is not currently active and meets the minimum
1746
		 * plugin version requirements set in TGMPA (if any).
1747
		 *
1748
		 * @since 2.5.0
1749
		 *
1750
		 * @param string $slug Plugin slug.
1751
		 * @return bool True if OK to activate, false otherwise.
1752
		 */
1753
		public function can_plugin_activate( $slug ) {
1754
			return ( ! $this->is_plugin_active( $slug ) && ! $this->does_plugin_require_update( $slug ) );
1755
		}
1756
1757
		/**
1758
		 * Retrieve the version number of an installed plugin.
1759
		 *
1760
		 * @since 2.5.0
1761
		 *
1762
		 * @param string $slug Plugin slug.
1763
		 * @return string Version number as string or an empty string if the plugin is not installed
1764
		 *                or version unknown (plugins which don't comply with the plugin header standard).
1765
		 */
1766
		public function get_installed_version( $slug ) {
1767
			$installed_plugins = $this->get_plugins(); // Retrieve a list of all installed plugins (WP cached).
1768
1769
			if ( ! empty( $installed_plugins[ $this->plugins[ $slug ]['file_path'] ]['Version'] ) ) {
1770
				return $installed_plugins[ $this->plugins[ $slug ]['file_path'] ]['Version'];
1771
			}
1772
1773
			return '';
1774
		}
1775
1776
		/**
1777
		 * Check whether a plugin complies with the minimum version requirements.
1778
		 *
1779
		 * @since 2.5.0
1780
		 *
1781
		 * @param string $slug Plugin slug.
1782
		 * @return bool True when a plugin needs to be updated, otherwise false.
1783
		 */
1784
		public function does_plugin_require_update( $slug ) {
1785
			$installed_version = $this->get_installed_version( $slug );
1786
			$minimum_version   = $this->plugins[ $slug ]['version'];
1787
1788
			return version_compare( $minimum_version, $installed_version, '>' );
1789
		}
1790
1791
		/**
1792
		 * Check whether there is an update available for a plugin.
1793
		 *
1794
		 * @since 2.5.0
1795
		 *
1796
		 * @param string $slug Plugin slug.
1797
		 * @return false|string Version number string of the available update or false if no update available.
1798
		 */
1799
		public function does_plugin_have_update( $slug ) {
1800
			// Presume bundled and external plugins will point to a package which meets the minimum required version.
1801
			if ( 'repo' !== $this->plugins[ $slug ]['source_type'] ) {
1802
				if ( $this->does_plugin_require_update( $slug ) ) {
1803
					return $this->plugins[ $slug ]['version'];
1804
				}
1805
1806
				return false;
1807
			}
1808
1809
			$repo_updates = get_site_transient( 'update_plugins' );
1810
1811 View Code Duplication
			if ( isset( $repo_updates->response[ $this->plugins[ $slug ]['file_path'] ]->new_version ) ) {
1812
				return $repo_updates->response[ $this->plugins[ $slug ]['file_path'] ]->new_version;
1813
			}
1814
1815
			return false;
1816
		}
1817
1818
		/**
1819
		 * Retrieve potential upgrade notice for a plugin.
1820
		 *
1821
		 * @since 2.5.0
1822
		 *
1823
		 * @param string $slug Plugin slug.
1824
		 * @return string The upgrade notice or an empty string if no message was available or provided.
1825
		 */
1826
		public function get_upgrade_notice( $slug ) {
1827
			// We currently can't get reliable info on non-WP-repo plugins - issue #380.
1828
			if ( 'repo' !== $this->plugins[ $slug ]['source_type'] ) {
1829
				return '';
1830
			}
1831
1832
			$repo_updates = get_site_transient( 'update_plugins' );
1833
1834 View Code Duplication
			if ( ! empty( $repo_updates->response[ $this->plugins[ $slug ]['file_path'] ]->upgrade_notice ) ) {
1835
				return $repo_updates->response[ $this->plugins[ $slug ]['file_path'] ]->upgrade_notice;
1836
			}
1837
1838
			return '';
1839
		}
1840
1841
		/**
1842
		 * Wrapper around the core WP get_plugins function, making sure it's actually available.
1843
		 *
1844
		 * @since 2.5.0
1845
		 *
1846
		 * @param string $plugin_folder Optional. Relative path to single plugin folder.
1847
		 * @return array Array of installed plugins with plugin information.
1848
		 */
1849
		public function get_plugins( $plugin_folder = '' ) {
1850
			if ( ! function_exists( 'get_plugins' ) ) {
1851
				require_once ABSPATH . 'wp-admin/includes/plugin.php';
1852
			}
1853
1854
			return get_plugins( $plugin_folder );
1855
		}
1856
1857
		/**
1858
		 * Delete dismissable nag option when theme is switched.
1859
		 *
1860
		 * This ensures that the user(s) is/are again reminded via nag of required
1861
		 * and/or recommended plugins if they re-activate the theme.
1862
		 *
1863
		 * @since 2.1.1
1864
		 */
1865
		public function update_dismiss() {
1866
			delete_metadata( 'user', null, 'tgmpa_dismissed_notice_' . $this->id, null, true );
1867
		}
1868
1869
		/**
1870
		 * Forces plugin activation if the parameter 'force_activation' is
1871
		 * set to true.
1872
		 *
1873
		 * This allows theme authors to specify certain plugins that must be
1874
		 * active at all times while using the current theme.
1875
		 *
1876
		 * Please take special care when using this parameter as it has the
1877
		 * potential to be harmful if not used correctly. Setting this parameter
1878
		 * to true will not allow the specified plugin to be deactivated unless
1879
		 * the user switches themes.
1880
		 *
1881
		 * @since 2.2.0
1882
		 */
1883
		public function force_activation() {
1884
			foreach ( $this->plugins as $slug => $plugin ) {
1885
				if ( true === $plugin['force_activation'] ) {
1886
					if ( ! $this->is_plugin_installed( $slug ) ) {
1887
						// Oops, plugin isn't there so iterate to next condition.
1888
						continue;
1889
					} elseif ( $this->can_plugin_activate( $slug ) ) {
1890
						// There we go, activate the plugin.
1891
						activate_plugin( $plugin['file_path'] );
1892
					}
1893
				}
1894
			}
1895
		}
1896
1897
		/**
1898
		 * Forces plugin deactivation if the parameter 'force_deactivation'
1899
		 * is set to true.
1900
		 *
1901
		 * This allows theme authors to specify certain plugins that must be
1902
		 * deactivated upon switching from the current theme to another.
1903
		 *
1904
		 * Please take special care when using this parameter as it has the
1905
		 * potential to be harmful if not used correctly.
1906
		 *
1907
		 * @since 2.2.0
1908
		 */
1909
		public function force_deactivation() {
1910
			foreach ( $this->plugins as $slug => $plugin ) {
1911
				// Only proceed forward if the parameter is set to true and plugin is active.
1912
				if ( true === $plugin['force_deactivation'] && $this->is_plugin_active( $slug ) ) {
1913
					deactivate_plugins( $plugin['file_path'] );
1914
				}
1915
			}
1916
		}
1917
1918
		/**
1919
		 * Echo the current TGMPA version number to the page.
1920
		 */
1921
		public function show_tgmpa_version() {
1922
			echo '<p style="float: right; padding: 0em 1.5em 0.5em 0;"><strong><small>',
1923
				esc_html( sprintf( _x( 'TGMPA v%s', '%s = version number', 'tgmpa' ), self::TGMPA_VERSION ) ),
1924
				'</small></strong></p>';
1925
		}
1926
1927
		/**
1928
		 * Returns the singleton instance of the class.
1929
		 *
1930
		 * @since 2.4.0
1931
		 *
1932
		 * @return object The TGM_Plugin_Activation object.
1933
		 */
1934
		public static function get_instance() {
1935
			if ( ! isset( self::$instance ) && ! ( self::$instance instanceof self ) ) {
1936
				self::$instance = new self();
1937
			}
1938
1939
			return self::$instance;
1940
		}
1941
	}
1942
1943
	if ( ! function_exists( 'load_tgm_plugin_activation' ) ) {
1944
		/**
1945
		 * Ensure only one instance of the class is ever invoked.
1946
		 */
1947
		function load_tgm_plugin_activation() {
1948
			$GLOBALS['tgmpa'] = TGM_Plugin_Activation::get_instance();
1949
		}
1950
	}
1951
1952
	if ( did_action( 'plugins_loaded' ) ) {
1953
		load_tgm_plugin_activation();
1954
	} else {
1955
		add_action( 'plugins_loaded', 'load_tgm_plugin_activation' );
1956
	}
1957
}
1958
1959
if ( ! function_exists( 'tgmpa' ) ) {
1960
	/**
1961
	 * Helper function to register a collection of required plugins.
1962
	 *
1963
	 * @since 2.0.0
1964
	 * @api
1965
	 *
1966
	 * @param array $plugins An array of plugin arrays.
1967
	 * @param array $config  Optional. An array of configuration values.
1968
	 */
1969
	function tgmpa( $plugins, $config = array() ) {
1970
		$instance = call_user_func( array( get_class( $GLOBALS['tgmpa'] ), 'get_instance' ) );
1971
1972
		foreach ( $plugins as $plugin ) {
1973
			call_user_func( array( $instance, 'register' ), $plugin );
1974
		}
1975
1976
		if ( ! empty( $config ) && is_array( $config ) ) {
1977
			// Send out notices for deprecated arguments passed.
1978
			if ( isset( $config['notices'] ) ) {
1979
				_deprecated_argument( __FUNCTION__, '2.2.0', 'The `notices` config parameter was renamed to `has_notices` in TGMPA 2.2.0. Please adjust your configuration.' );
1980
				if ( ! isset( $config['has_notices'] ) ) {
1981
					$config['has_notices'] = $config['notices'];
1982
				}
1983
			}
1984
1985
			if ( isset( $config['parent_menu_slug'] ) ) {
1986
				_deprecated_argument( __FUNCTION__, '2.4.0', 'The `parent_menu_slug` config parameter was removed in TGMPA 2.4.0. In TGMPA 2.5.0 an alternative was (re-)introduced. Please adjust your configuration. For more information visit the website: http://tgmpluginactivation.com/configuration/#h-configuration-options.' );
1987
			}
1988
			if ( isset( $config['parent_url_slug'] ) ) {
1989
				_deprecated_argument( __FUNCTION__, '2.4.0', 'The `parent_url_slug` config parameter was removed in TGMPA 2.4.0. In TGMPA 2.5.0 an alternative was (re-)introduced. Please adjust your configuration. For more information visit the website: http://tgmpluginactivation.com/configuration/#h-configuration-options.' );
1990
			}
1991
1992
			call_user_func( array( $instance, 'config' ), $config );
1993
		}
1994
	}
1995
}
1996
1997
/**
1998
 * WP_List_Table isn't always available. If it isn't available,
1999
 * we load it here.
2000
 *
2001
 * @since 2.2.0
2002
 */
2003
if ( ! class_exists( 'WP_List_Table' ) ) {
2004
	require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php';
2005
}
2006
2007
if ( ! class_exists( 'TGMPA_List_Table' ) ) {
2008
2009
	/**
2010
	 * List table class for handling plugins.
2011
	 *
2012
	 * Extends the WP_List_Table class to provide a future-compatible
2013
	 * way of listing out all required/recommended plugins.
2014
	 *
2015
	 * Gives users an interface similar to the Plugin Administration
2016
	 * area with similar (albeit stripped down) capabilities.
2017
	 *
2018
	 * This class also allows for the bulk install of plugins.
2019
	 *
2020
	 * @since 2.2.0
2021
	 *
2022
	 * @package TGM-Plugin-Activation
2023
	 * @author  Thomas Griffin
2024
	 * @author  Gary Jones
2025
	 */
2026
	class TGMPA_List_Table extends WP_List_Table {
2027
		/**
2028
		 * TGMPA instance.
2029
		 *
2030
		 * @since 2.5.0
2031
		 *
2032
		 * @var object
2033
		 */
2034
		protected $tgmpa;
2035
2036
		/**
2037
		 * The currently chosen view.
2038
		 *
2039
		 * @since 2.5.0
2040
		 *
2041
		 * @var string One of: 'all', 'install', 'update', 'activate'
2042
		 */
2043
		public $view_context = 'all';
2044
2045
		/**
2046
		 * The plugin counts for the various views.
2047
		 *
2048
		 * @since 2.5.0
2049
		 *
2050
		 * @var array
2051
		 */
2052
		protected $view_totals = array(
2053
			'all'      => 0,
2054
			'install'  => 0,
2055
			'update'   => 0,
2056
			'activate' => 0,
2057
		);
2058
2059
		/**
2060
		 * References parent constructor and sets defaults for class.
2061
		 *
2062
		 * @since 2.2.0
2063
		 */
2064
		public function __construct() {
2065
			$this->tgmpa = call_user_func( array( get_class( $GLOBALS['tgmpa'] ), 'get_instance' ) );
2066
2067
			parent::__construct(
2068
				array(
2069
					'singular' => 'plugin',
2070
					'plural'   => 'plugins',
2071
					'ajax'     => false,
2072
				)
2073
			);
2074
2075
			if ( isset( $_REQUEST['plugin_status'] ) && in_array( $_REQUEST['plugin_status'], array( 'install', 'update', 'activate' ), true ) ) {
2076
				$this->view_context = sanitize_key( $_REQUEST['plugin_status'] );
2077
			}
2078
2079
			add_filter( 'tgmpa_table_data_items', array( $this, 'sort_table_items' ) );
2080
		}
2081
2082
		/**
2083
		 * Get a list of CSS classes for the <table> tag.
2084
		 *
2085
		 * Overruled to prevent the 'plural' argument from being added.
2086
		 *
2087
		 * @since 2.5.0
2088
		 *
2089
		 * @return array CSS classnames.
2090
		 */
2091
		public function get_table_classes() {
2092
			return array( 'widefat', 'fixed' );
2093
		}
2094
2095
		/**
2096
		 * Gathers and renames all of our plugin information to be used by WP_List_Table to create our table.
2097
		 *
2098
		 * @since 2.2.0
2099
		 *
2100
		 * @return array $table_data Information for use in table.
2101
		 */
2102
		protected function _gather_plugin_data() {
2103
			// Load thickbox for plugin links.
2104
			$this->tgmpa->admin_init();
2105
			$this->tgmpa->thickbox();
2106
2107
			// Categorize the plugins which have open actions.
2108
			$plugins = $this->categorize_plugins_to_views();
2109
2110
			// Set the counts for the view links.
2111
			$this->set_view_totals( $plugins );
2112
2113
			// Prep variables for use and grab list of all installed plugins.
2114
			$table_data = array();
2115
			$i          = 0;
2116
2117
			// Redirect to the 'all' view if no plugins were found for the selected view context.
2118
			if ( empty( $plugins[ $this->view_context ] ) ) {
2119
				$this->view_context = 'all';
2120
			}
2121
2122
			foreach ( $plugins[ $this->view_context ] as $slug => $plugin ) {
2123
				$table_data[ $i ]['sanitized_plugin']  = $plugin['name'];
2124
				$table_data[ $i ]['slug']              = $slug;
2125
				$table_data[ $i ]['plugin']            = '<strong>' . $this->tgmpa->get_info_link( $slug ) . '</strong>';
2126
				$table_data[ $i ]['source']            = $this->get_plugin_source_type_text( $plugin['source_type'] );
2127
				$table_data[ $i ]['type']              = $this->get_plugin_advise_type_text( $plugin['required'] );
2128
				$table_data[ $i ]['status']            = $this->get_plugin_status_text( $slug );
2129
				$table_data[ $i ]['installed_version'] = $this->tgmpa->get_installed_version( $slug );
2130
				$table_data[ $i ]['minimum_version']   = $plugin['version'];
2131
				$table_data[ $i ]['available_version'] = $this->tgmpa->does_plugin_have_update( $slug );
2132
2133
				// Prep the upgrade notice info.
2134
				$upgrade_notice = $this->tgmpa->get_upgrade_notice( $slug );
2135
				if ( ! empty( $upgrade_notice ) ) {
2136
					$table_data[ $i ]['upgrade_notice'] = $upgrade_notice;
2137
2138
					add_action( "tgmpa_after_plugin_row_$slug", array( $this, 'wp_plugin_update_row' ), 10, 2 );
2139
				}
2140
2141
				$table_data[ $i ] = apply_filters( 'tgmpa_table_data_item', $table_data[ $i ], $plugin );
2142
2143
				$i++;
2144
			}
2145
2146
			return $table_data;
2147
		}
2148
2149
		/**
2150
		 * Categorize the plugins which have open actions into views for the TGMPA page.
2151
		 *
2152
		 * @since 2.5.0
2153
		 */
2154
		protected function categorize_plugins_to_views() {
2155
			$plugins = array(
2156
				'all'      => array(), // Meaning: all plugins which still have open actions.
2157
				'install'  => array(),
2158
				'update'   => array(),
2159
				'activate' => array(),
2160
			);
2161
2162
			foreach ( $this->tgmpa->plugins as $slug => $plugin ) {
2163
				if ( $this->tgmpa->is_plugin_active( $slug ) && false === $this->tgmpa->does_plugin_have_update( $slug ) ) {
2164
					// No need to display plugins if they are installed, up-to-date and active.
2165
					continue;
2166
				} else {
2167
					$plugins['all'][ $slug ] = $plugin;
2168
2169
					if ( ! $this->tgmpa->is_plugin_installed( $slug ) ) {
2170
						$plugins['install'][ $slug ] = $plugin;
2171
					} else {
2172
						if ( false !== $this->tgmpa->does_plugin_have_update( $slug ) ) {
2173
							$plugins['update'][ $slug ] = $plugin;
2174
						}
2175
2176
						if ( $this->tgmpa->can_plugin_activate( $slug ) ) {
2177
							$plugins['activate'][ $slug ] = $plugin;
2178
						}
2179
					}
2180
				}
2181
			}
2182
2183
			return $plugins;
2184
		}
2185
2186
		/**
2187
		 * Set the counts for the view links.
2188
		 *
2189
		 * @since 2.5.0
2190
		 *
2191
		 * @param array $plugins Plugins order by view.
2192
		 */
2193
		protected function set_view_totals( $plugins ) {
2194
			foreach ( $plugins as $type => $list ) {
2195
				$this->view_totals[ $type ] = count( $list );
2196
			}
2197
		}
2198
2199
		/**
2200
		 * Get the plugin required/recommended text string.
2201
		 *
2202
		 * @since 2.5.0
2203
		 *
2204
		 * @param string $required Plugin required setting.
2205
		 * @return string
2206
		 */
2207
		protected function get_plugin_advise_type_text( $required ) {
2208
			if ( true === $required ) {
2209
				return __( 'Required', 'tgmpa' );
2210
			}
2211
2212
			return __( 'Recommended', 'tgmpa' );
2213
		}
2214
2215
		/**
2216
		 * Get the plugin source type text string.
2217
		 *
2218
		 * @since 2.5.0
2219
		 *
2220
		 * @param string $type Plugin type.
2221
		 * @return string
2222
		 */
2223
		protected function get_plugin_source_type_text( $type ) {
2224
			$string = '';
2225
2226
			switch ( $type ) {
2227
				case 'repo':
2228
					$string = __( 'WordPress Repository', 'tgmpa' );
2229
					break;
2230
				case 'external':
2231
					$string = __( 'External Source', 'tgmpa' );
2232
					break;
2233
				case 'bundled':
2234
					$string = __( 'Pre-Packaged', 'tgmpa' );
2235
					break;
2236
			}
2237
2238
			return $string;
2239
		}
2240
2241
		/**
2242
		 * Determine the plugin status message.
2243
		 *
2244
		 * @since 2.5.0
2245
		 *
2246
		 * @param string $slug Plugin slug.
2247
		 * @return string
2248
		 */
2249
		protected function get_plugin_status_text( $slug ) {
2250
			if ( ! $this->tgmpa->is_plugin_installed( $slug ) ) {
2251
				return __( 'Not Installed', 'tgmpa' );
2252
			}
2253
2254
			if ( ! $this->tgmpa->is_plugin_active( $slug ) ) {
2255
				$install_status = __( 'Installed But Not Activated', 'tgmpa' );
2256
			} else {
2257
				$install_status = __( 'Active', 'tgmpa' );
2258
			}
2259
2260
			$update_status = '';
2261
2262
			if ( $this->tgmpa->does_plugin_require_update( $slug ) && false === $this->tgmpa->does_plugin_have_update( $slug ) ) {
2263
				$update_status = __( 'Required Update not Available', 'tgmpa' );
2264
2265
			} elseif ( $this->tgmpa->does_plugin_require_update( $slug ) ) {
2266
				$update_status = __( 'Requires Update', 'tgmpa' );
2267
2268
			} elseif ( false !== $this->tgmpa->does_plugin_have_update( $slug ) ) {
2269
				$update_status = __( 'Update recommended', 'tgmpa' );
2270
			}
2271
2272
			if ( '' === $update_status ) {
2273
				return $install_status;
2274
			}
2275
2276
			return sprintf(
2277
				_x( '%1$s, %2$s', '%1$s = install status, %2$s = update status', 'tgmpa' ),
2278
				$install_status,
2279
				$update_status
2280
			);
2281
		}
2282
2283
		/**
2284
		 * Sort plugins by Required/Recommended type and by alphabetical plugin name within each type.
2285
		 *
2286
		 * @since 2.5.0
2287
		 *
2288
		 * @param array $items Prepared table items.
2289
		 * @return array Sorted table items.
2290
		 */
2291
		public function sort_table_items( $items ) {
2292
			$type = array();
2293
			$name = array();
2294
2295
			foreach ( $items as $i => $plugin ) {
2296
				$type[ $i ] = $plugin['type']; // Required / recommended.
2297
				$name[ $i ] = $plugin['sanitized_plugin'];
2298
			}
2299
2300
			array_multisort( $type, SORT_DESC, $name, SORT_ASC, $items );
2301
2302
			return $items;
2303
		}
2304
2305
		/**
2306
		 * Get an associative array ( id => link ) of the views available on this table.
2307
		 *
2308
		 * @since 2.5.0
2309
		 *
2310
		 * @return array
2311
		 */
2312
		public function get_views() {
2313
			$status_links = array();
2314
2315
			foreach ( $this->view_totals as $type => $count ) {
2316
				if ( $count < 1 ) {
2317
					continue;
2318
				}
2319
2320
				switch ( $type ) {
2321
					case 'all':
2322
						$text = _nx( 'All <span class="count">(%s)</span>', 'All <span class="count">(%s)</span>', $count, 'plugins', 'tgmpa' );
2323
						break;
2324
					case 'install':
2325
						$text = _n( 'To Install <span class="count">(%s)</span>', 'To Install <span class="count">(%s)</span>', $count, 'tgmpa' );
2326
						break;
2327
					case 'update':
2328
						$text = _n( 'Update Available <span class="count">(%s)</span>', 'Update Available <span class="count">(%s)</span>', $count, 'tgmpa' );
2329
						break;
2330
					case 'activate':
2331
						$text = _n( 'To Activate <span class="count">(%s)</span>', 'To Activate <span class="count">(%s)</span>', $count, 'tgmpa' );
2332
						break;
2333
					default:
2334
						$text = '';
2335
						break;
2336
				}
2337
2338
				if ( ! empty( $text ) ) {
2339
2340
					$status_links[ $type ] = sprintf(
2341
						'<a href="%s"%s>%s</a>',
2342
						esc_url( $this->tgmpa->get_tgmpa_status_url( $type ) ),
2343
						( $type === $this->view_context ) ? ' class="current"' : '',
2344
						sprintf( $text, number_format_i18n( $count ) )
2345
					);
2346
				}
2347
			}
2348
2349
			return $status_links;
2350
		}
2351
2352
		/**
2353
		 * Create default columns to display important plugin information
2354
		 * like type, action and status.
2355
		 *
2356
		 * @since 2.2.0
2357
		 *
2358
		 * @param array  $item        Array of item data.
2359
		 * @param string $column_name The name of the column.
2360
		 * @return string
2361
		 */
2362
		public function column_default( $item, $column_name ) {
2363
			return $item[ $column_name ];
2364
		}
2365
2366
		/**
2367
		 * Required for bulk installing.
2368
		 *
2369
		 * Adds a checkbox for each plugin.
2370
		 *
2371
		 * @since 2.2.0
2372
		 *
2373
		 * @param array $item Array of item data.
2374
		 * @return string The input checkbox with all necessary info.
2375
		 */
2376
		public function column_cb( $item ) {
2377
			return sprintf(
2378
				'<input type="checkbox" name="%1$s[]" value="%2$s" id="%3$s" />',
2379
				esc_attr( $this->_args['singular'] ),
2380
				esc_attr( $item['slug'] ),
2381
				esc_attr( $item['sanitized_plugin'] )
2382
			);
2383
		}
2384
2385
		/**
2386
		 * Create default title column along with the action links.
2387
		 *
2388
		 * @since 2.2.0
2389
		 *
2390
		 * @param array $item Array of item data.
2391
		 * @return string The plugin name and action links.
2392
		 */
2393
		public function column_plugin( $item ) {
2394
			return sprintf(
2395
				'%1$s %2$s',
2396
				$item['plugin'],
2397
				$this->row_actions( $this->get_row_actions( $item ), true )
2398
			);
2399
		}
2400
2401
		/**
2402
		 * Create version information column.
2403
		 *
2404
		 * @since 2.5.0
2405
		 *
2406
		 * @param array $item Array of item data.
2407
		 * @return string HTML-formatted version information.
2408
		 */
2409
		public function column_version( $item ) {
2410
			$output = array();
2411
2412
			if ( $this->tgmpa->is_plugin_installed( $item['slug'] ) ) {
2413
				$installed = ! empty( $item['installed_version'] ) ? $item['installed_version'] : _x( 'unknown', 'as in: "version nr unknown"', 'tgmpa' );
2414
2415
				$color = '';
2416
				if ( ! empty( $item['minimum_version'] ) && $this->tgmpa->does_plugin_require_update( $item['slug'] ) ) {
2417
					$color = ' color: #ff0000; font-weight: bold;';
2418
				}
2419
2420
				$output[] = sprintf(
2421
					'<p><span style="min-width: 32px; text-align: right; float: right;%1$s">%2$s</span>' . __( 'Installed version:', 'tgmpa' ) . '</p>',
2422
					$color,
2423
					$installed
2424
				);
2425
			}
2426
2427
			if ( ! empty( $item['minimum_version'] ) ) {
2428
				$output[] = sprintf(
2429
					'<p><span style="min-width: 32px; text-align: right; float: right;">%1$s</span>' . __( 'Minimum required version:', 'tgmpa' ) . '</p>',
2430
					$item['minimum_version']
2431
				);
2432
			}
2433
2434
			if ( ! empty( $item['available_version'] ) ) {
2435
				$color = '';
2436
				if ( ! empty( $item['minimum_version'] ) && version_compare( $item['available_version'], $item['minimum_version'], '>=' ) ) {
2437
					$color = ' color: #71C671; font-weight: bold;';
2438
				}
2439
2440
				$output[] = sprintf(
2441
					'<p><span style="min-width: 32px; text-align: right; float: right;%1$s">%2$s</span>' . __( 'Available version:', 'tgmpa' ) . '</p>',
2442
					$color,
2443
					$item['available_version']
2444
				);
2445
			}
2446
2447
			if ( empty( $output ) ) {
2448
				return '&nbsp;'; // Let's not break the table layout.
2449
			} else {
2450
				return implode( "\n", $output );
2451
			}
2452
		}
2453
2454
		/**
2455
		 * Sets default message within the plugins table if no plugins
2456
		 * are left for interaction.
2457
		 *
2458
		 * Hides the menu item to prevent the user from clicking and
2459
		 * getting a permissions error.
2460
		 *
2461
		 * @since 2.2.0
2462
		 */
2463
		public function no_items() {
2464
			printf( wp_kses_post( __( 'No plugins to install, update or activate. <a href="%1$s">Return to the Dashboard</a>', 'tgmpa' ) ), esc_url( self_admin_url() ) );
2465
			echo '<style type="text/css">#adminmenu .wp-submenu li.current { display: none !important; }</style>';
2466
		}
2467
2468
		/**
2469
		 * Output all the column information within the table.
2470
		 *
2471
		 * @since 2.2.0
2472
		 *
2473
		 * @return array $columns The column names.
2474
		 */
2475
		public function get_columns() {
2476
			$columns = array(
2477
				'cb'     => '<input type="checkbox" />',
2478
				'plugin' => __( 'Plugin', 'tgmpa' ),
2479
				'source' => __( 'Source', 'tgmpa' ),
2480
				'type'   => __( 'Type', 'tgmpa' ),
2481
			);
2482
2483
			if ( 'all' === $this->view_context || 'update' === $this->view_context ) {
2484
				$columns['version'] = __( 'Version', 'tgmpa' );
2485
				$columns['status']  = __( 'Status', 'tgmpa' );
2486
			}
2487
2488
			return apply_filters( 'tgmpa_table_columns', $columns );
2489
		}
2490
2491
		/**
2492
		 * Get name of default primary column
2493
		 *
2494
		 * @since 2.5.0 / WP 4.3+ compatibility
2495
		 * @access protected
2496
		 *
2497
		 * @return string
2498
		 */
2499
		protected function get_default_primary_column_name() {
2500
			return 'plugin';
2501
		}
2502
2503
		/**
2504
		 * Get the name of the primary column.
2505
		 *
2506
		 * @since 2.5.0 / WP 4.3+ compatibility
2507
		 * @access protected
2508
		 *
2509
		 * @return string The name of the primary column.
2510
		 */
2511
		protected function get_primary_column_name() {
2512
			if ( method_exists( 'WP_List_Table', 'get_primary_column_name' ) ) {
2513
				return parent::get_primary_column_name();
2514
			} else {
2515
				return $this->get_default_primary_column_name();
2516
			}
2517
		}
2518
2519
		/**
2520
		 * Get the actions which are relevant for a specific plugin row.
2521
		 *
2522
		 * @since 2.5.0
2523
		 *
2524
		 * @param array $item Array of item data.
2525
		 * @return array Array with relevant action links.
2526
		 */
2527
		protected function get_row_actions( $item ) {
2528
			$actions      = array();
2529
			$action_links = array();
2530
2531
			// Display the 'Install' action link if the plugin is not yet available.
2532
			if ( ! $this->tgmpa->is_plugin_installed( $item['slug'] ) ) {
2533
				$actions['install'] = _x( 'Install %2$s', '%2$s = plugin name in screen reader markup', 'tgmpa' );
2534
			} else {
2535
				// Display the 'Update' action link if an update is available and WP complies with plugin minimum.
2536
				if ( false !== $this->tgmpa->does_plugin_have_update( $item['slug'] ) && $this->tgmpa->can_plugin_update( $item['slug'] ) ) {
2537
					$actions['update'] = _x( 'Update %2$s', '%2$s = plugin name in screen reader markup', 'tgmpa' );
2538
				}
2539
2540
				// Display the 'Activate' action link, but only if the plugin meets the minimum version.
2541
				if ( $this->tgmpa->can_plugin_activate( $item['slug'] ) ) {
2542
					$actions['activate'] = _x( 'Activate %2$s', '%2$s = plugin name in screen reader markup', 'tgmpa' );
2543
				}
2544
			}
2545
2546
			// Create the actual links.
2547
			foreach ( $actions as $action => $text ) {
2548
				$nonce_url = wp_nonce_url(
2549
					add_query_arg(
2550
						array(
2551
							'plugin'           => urlencode( $item['slug'] ),
2552
							'tgmpa-' . $action => $action . '-plugin',
2553
						),
2554
						$this->tgmpa->get_tgmpa_url()
2555
					),
2556
					'tgmpa-' . $action,
2557
					'tgmpa-nonce'
2558
				);
2559
2560
				$action_links[ $action ] = sprintf(
2561
					'<a href="%1$s">' . esc_html( $text ) . '</a>',
2562
					esc_url( $nonce_url ),
2563
					'<span class="screen-reader-text">' . esc_html( $item['sanitized_plugin'] ) . '</span>'
2564
				);
2565
			}
2566
2567
			$prefix = ( defined( 'WP_NETWORK_ADMIN' ) && WP_NETWORK_ADMIN ) ? 'network_admin_' : '';
2568
			return apply_filters( "tgmpa_{$prefix}plugin_action_links", array_filter( $action_links ), $item['slug'], $item, $this->view_context );
2569
		}
2570
2571
		/**
2572
		 * Generates content for a single row of the table.
2573
		 *
2574
		 * @since 2.5.0
2575
		 *
2576
		 * @param object $item The current item.
2577
		 */
2578
		public function single_row( $item ) {
2579
			parent::single_row( $item );
2580
2581
			/**
2582
			 * Fires after each specific row in the TGMPA Plugins list table.
2583
			 *
2584
			 * The dynamic portion of the hook name, `$item['slug']`, refers to the slug
2585
			 * for the plugin.
2586
			 *
2587
			 * @since 2.5.0
2588
			 */
2589
			do_action( "tgmpa_after_plugin_row_{$item['slug']}", $item['slug'], $item, $this->view_context );
2590
		}
2591
2592
		/**
2593
		 * Show the upgrade notice below a plugin row if there is one.
2594
		 *
2595
		 * @since 2.5.0
2596
		 *
2597
		 * @see /wp-admin/includes/update.php
2598
		 *
2599
		 * @param string $slug Plugin slug.
2600
		 * @param array  $item The information available in this table row.
2601
		 * @return null Return early if upgrade notice is empty.
2602
		 */
2603
		public function wp_plugin_update_row( $slug, $item ) {
2604
			if ( empty( $item['upgrade_notice'] ) ) {
2605
				return;
2606
			}
2607
2608
			echo '
2609
				<tr class="plugin-update-tr">
2610
					<td colspan="', absint( $this->get_column_count() ), '" class="plugin-update colspanchange">
2611
						<div class="update-message">',
2612
							esc_html__( 'Upgrade message from the plugin author:', 'tgmpa' ),
2613
							' <strong>', wp_kses_data( $item['upgrade_notice'] ), '</strong>
2614
						</div>
2615
					</td>
2616
				</tr>';
2617
		}
2618
2619
		/**
2620
		 * Extra controls to be displayed between bulk actions and pagination.
2621
		 *
2622
		 * @since 2.5.0
2623
		 *
2624
		 * @param string $which 'top' or 'bottom' table navigation.
2625
		 */
2626
		public function extra_tablenav( $which ) {
2627
			if ( 'bottom' === $which ) {
2628
				$this->tgmpa->show_tgmpa_version();
2629
			}
2630
		}
2631
2632
		/**
2633
		 * Defines the bulk actions for handling registered plugins.
2634
		 *
2635
		 * @since 2.2.0
2636
		 *
2637
		 * @return array $actions The bulk actions for the plugin install table.
2638
		 */
2639
		public function get_bulk_actions() {
2640
2641
			$actions = array();
2642
2643
			if ( 'update' !== $this->view_context && 'activate' !== $this->view_context ) {
2644
				if ( current_user_can( 'install_plugins' ) ) {
2645
					$actions['tgmpa-bulk-install'] = __( 'Install', 'tgmpa' );
2646
				}
2647
			}
2648
2649
			if ( 'install' !== $this->view_context ) {
2650
				if ( current_user_can( 'update_plugins' ) ) {
2651
					$actions['tgmpa-bulk-update'] = __( 'Update', 'tgmpa' );
2652
				}
2653
				if ( current_user_can( 'activate_plugins' ) ) {
2654
					$actions['tgmpa-bulk-activate'] = __( 'Activate', 'tgmpa' );
2655
				}
2656
			}
2657
2658
			return $actions;
2659
		}
2660
2661
		/**
2662
		 * Processes bulk installation and activation actions.
2663
		 *
2664
		 * The bulk installation process looks for the $_POST information and passes that
2665
		 * through if a user has to use WP_Filesystem to enter their credentials.
2666
		 *
2667
		 * @since 2.2.0
2668
		 */
2669
		public function process_bulk_actions() {
2670
			// Bulk installation process.
2671
			if ( 'tgmpa-bulk-install' === $this->current_action() || 'tgmpa-bulk-update' === $this->current_action() ) {
2672
2673
				check_admin_referer( 'bulk-' . $this->_args['plural'] );
2674
2675
				$install_type = 'install';
2676
				if ( 'tgmpa-bulk-update' === $this->current_action() ) {
2677
					$install_type = 'update';
2678
				}
2679
2680
				$plugins_to_install = array();
2681
2682
				// Did user actually select any plugins to install/update ?
2683 View Code Duplication
				if ( empty( $_POST['plugin'] ) ) {
2684
					if ( 'install' === $install_type ) {
2685
						$message = __( 'No plugins were selected to be installed. No action taken.', 'tgmpa' );
2686
					} else {
2687
						$message = __( 'No plugins were selected to be updated. No action taken.', 'tgmpa' );
2688
					}
2689
2690
					echo '<div id="message" class="error"><p>', esc_html( $message ), '</p></div>';
2691
2692
					return false;
2693
				}
2694
2695
				if ( is_array( $_POST['plugin'] ) ) {
2696
					$plugins_to_install = (array) $_POST['plugin'];
2697
				} elseif ( is_string( $_POST['plugin'] ) ) {
2698
					// Received via Filesystem page - un-flatten array (WP bug #19643).
2699
					$plugins_to_install = explode( ',', $_POST['plugin'] );
2700
				}
2701
2702
				// Sanitize the received input.
2703
				$plugins_to_install = array_map( 'urldecode', $plugins_to_install );
2704
				$plugins_to_install = array_map( array( $this->tgmpa, 'sanitize_key' ), $plugins_to_install );
2705
2706
				// Validate the received input.
2707
				foreach ( $plugins_to_install as $key => $slug ) {
2708
					// Check if the plugin was registered with TGMPA and remove if not.
2709
					if ( ! isset( $this->tgmpa->plugins[ $slug ] ) ) {
2710
						unset( $plugins_to_install[ $key ] );
2711
						continue;
2712
					}
2713
2714
					// For install: make sure this is a plugin we *can* install and not one already installed.
2715
					if ( 'install' === $install_type && true === $this->tgmpa->is_plugin_installed( $slug ) ) {
2716
						unset( $plugins_to_install[ $key ] );
2717
					}
2718
2719
					// For updates: make sure this is a plugin we *can* update (update available and WP version ok).
2720
					if ( 'update' === $install_type && false === $this->tgmpa->is_plugin_updatetable( $slug ) ) {
2721
						unset( $plugins_to_install[ $key ] );
2722
					}
2723
				}
2724
2725
				// No need to proceed further if we have no plugins to handle.
2726 View Code Duplication
				if ( empty( $plugins_to_install ) ) {
2727
					if ( 'install' === $install_type ) {
2728
						$message = __( 'No plugins are available to be installed at this time.', 'tgmpa' );
2729
					} else {
2730
						$message = __( 'No plugins are available to be updated at this time.', 'tgmpa' );
2731
					}
2732
2733
					echo '<div id="message" class="error"><p>', esc_html( $message ), '</p></div>';
2734
2735
					return false;
2736
				}
2737
2738
				// Pass all necessary information if WP_Filesystem is needed.
2739
				$url = wp_nonce_url(
2740
					$this->tgmpa->get_tgmpa_url(),
2741
					'bulk-' . $this->_args['plural']
2742
				);
2743
2744
				// Give validated data back to $_POST which is the only place the filesystem looks for extra fields.
2745
				$_POST['plugin'] = implode( ',', $plugins_to_install ); // Work around for WP bug #19643.
2746
2747
				$method = ''; // Leave blank so WP_Filesystem can populate it as necessary.
2748
				$fields = array_keys( $_POST ); // Extra fields to pass to WP_Filesystem.
2749
2750 View Code Duplication
				if ( false === ( $creds = request_filesystem_credentials( esc_url_raw( $url ), $method, false, false, $fields ) ) ) {
2751
					return true; // Stop the normal page form from displaying, credential request form will be shown.
2752
				}
2753
2754
				// Now we have some credentials, setup WP_Filesystem.
2755 View Code Duplication
				if ( ! WP_Filesystem( $creds ) ) {
2756
					// Our credentials were no good, ask the user for them again.
2757
					request_filesystem_credentials( esc_url_raw( $url ), $method, true, false, $fields );
2758
2759
					return true;
2760
				}
2761
2762
				/* If we arrive here, we have the filesystem */
2763
2764
				// Store all information in arrays since we are processing a bulk installation.
2765
				$names      = array();
2766
				$sources    = array(); // Needed for installs.
2767
				$file_paths = array(); // Needed for upgrades.
2768
				$to_inject  = array(); // Information to inject into the update_plugins transient.
2769
2770
				// Prepare the data for validated plugins for the install/upgrade.
2771
				foreach ( $plugins_to_install as $slug ) {
2772
					$name   = $this->tgmpa->plugins[ $slug ]['name'];
2773
					$source = $this->tgmpa->get_download_url( $slug );
2774
2775
					if ( ! empty( $name ) && ! empty( $source ) ) {
2776
						$names[] = $name;
2777
2778
						switch ( $install_type ) {
2779
2780
							case 'install':
2781
								$sources[] = $source;
2782
								break;
2783
2784
							case 'update':
2785
								$file_paths[]                 = $this->tgmpa->plugins[ $slug ]['file_path'];
2786
								$to_inject[ $slug ]           = $this->tgmpa->plugins[ $slug ];
2787
								$to_inject[ $slug ]['source'] = $source;
2788
								break;
2789
						}
2790
					}
2791
				}
2792
				unset( $slug, $name, $source );
2793
2794
				// Create a new instance of TGMPA_Bulk_Installer.
2795
				$installer = new TGMPA_Bulk_Installer(
2796
					new TGMPA_Bulk_Installer_Skin(
2797
						array(
2798
							'url'          => esc_url_raw( $this->tgmpa->get_tgmpa_url() ),
2799
							'nonce'        => 'bulk-' . $this->_args['plural'],
2800
							'names'        => $names,
2801
							'install_type' => $install_type,
2802
						)
2803
					)
2804
				);
2805
2806
				// Wrap the install process with the appropriate HTML.
2807
				echo '<div class="tgmpa">',
2808
					'<h2 style="font-size: 23px; font-weight: 400; line-height: 29px; margin: 0; padding: 9px 15px 4px 0;">', esc_html( get_admin_page_title() ), '</h2>
2809
					<div class="update-php" style="width: 100%; height: 98%; min-height: 850px; padding-top: 1px;">';
2810
2811
				// Process the bulk installation submissions.
2812
				add_filter( 'upgrader_source_selection', array( $this->tgmpa, 'maybe_adjust_source_dir' ), 1, 3 );
2813
2814
				if ( 'tgmpa-bulk-update' === $this->current_action() ) {
2815
					// Inject our info into the update transient.
2816
					$this->tgmpa->inject_update_info( $to_inject );
2817
2818
					$installer->bulk_upgrade( $file_paths );
2819
				} else {
2820
					$installer->bulk_install( $sources );
2821
				}
2822
2823
				remove_filter( 'upgrader_source_selection', array( $this->tgmpa, 'maybe_adjust_source_dir' ), 1, 3 );
2824
2825
				echo '</div></div>';
2826
2827
				return true;
2828
			}
2829
2830
			// Bulk activation process.
2831
			if ( 'tgmpa-bulk-activate' === $this->current_action() ) {
2832
				check_admin_referer( 'bulk-' . $this->_args['plural'] );
2833
2834
				// Did user actually select any plugins to activate ?
2835
				if ( empty( $_POST['plugin'] ) ) {
2836
					echo '<div id="message" class="error"><p>', esc_html__( 'No plugins were selected to be activated. No action taken.', 'tgmpa' ), '</p></div>';
2837
2838
					return false;
2839
				}
2840
2841
				// Grab plugin data from $_POST.
2842
				$plugins = array();
2843
				if ( isset( $_POST['plugin'] ) ) {
2844
					$plugins = array_map( 'urldecode', (array) $_POST['plugin'] );
2845
					$plugins = array_map( array( $this->tgmpa, 'sanitize_key' ), $plugins );
2846
				}
2847
2848
				$plugins_to_activate = array();
2849
				$plugin_names        = array();
2850
2851
				// Grab the file paths for the selected & inactive plugins from the registration array.
2852
				foreach ( $plugins as $slug ) {
2853
					if ( $this->tgmpa->can_plugin_activate( $slug ) ) {
2854
						$plugins_to_activate[] = $this->tgmpa->plugins[ $slug ]['file_path'];
2855
						$plugin_names[]        = $this->tgmpa->plugins[ $slug ]['name'];
2856
					}
2857
				}
2858
				unset( $slug );
2859
2860
				// Return early if there are no plugins to activate.
2861
				if ( empty( $plugins_to_activate ) ) {
2862
					echo '<div id="message" class="error"><p>', esc_html__( 'No plugins are available to be activated at this time.', 'tgmpa' ), '</p></div>';
2863
2864
					return false;
2865
				}
2866
2867
				// Now we are good to go - let's start activating plugins.
2868
				$activate = activate_plugins( $plugins_to_activate );
2869
2870
				if ( is_wp_error( $activate ) ) {
2871
					echo '<div id="message" class="error"><p>', wp_kses_post( $activate->get_error_message() ), '</p></div>';
2872
				} else {
2873
					$count        = count( $plugin_names ); // Count so we can use _n function.
2874
					$plugin_names = array_map( array( 'TGMPA_Utils', 'wrap_in_strong' ), $plugin_names );
2875
					$last_plugin  = array_pop( $plugin_names ); // Pop off last name to prep for readability.
2876
					$imploded     = empty( $plugin_names ) ? $last_plugin : ( implode( ', ', $plugin_names ) . ' ' . esc_html_x( 'and', 'plugin A *and* plugin B', 'tgmpa' ) . ' ' . $last_plugin );
2877
2878
					printf( // WPCS: xss ok.
2879
						'<div id="message" class="updated"><p>%1$s %2$s.</p></div>',
2880
						esc_html( _n( 'The following plugin was activated successfully:', 'The following plugins were activated successfully:', $count, 'tgmpa' ) ),
2881
						$imploded
2882
					);
2883
2884
					// Update recently activated plugins option.
2885
					$recent = (array) get_option( 'recently_activated' );
2886
					foreach ( $plugins_to_activate as $plugin => $time ) {
2887
						if ( isset( $recent[ $plugin ] ) ) {
2888
							unset( $recent[ $plugin ] );
2889
						}
2890
					}
2891
					update_option( 'recently_activated', $recent );
2892
				}
2893
2894
				unset( $_POST ); // Reset the $_POST variable in case user wants to perform one action after another.
2895
2896
				return true;
2897
			}
2898
2899
			return false;
2900
		}
2901
2902
		/**
2903
		 * Prepares all of our information to be outputted into a usable table.
2904
		 *
2905
		 * @since 2.2.0
2906
		 */
2907
		public function prepare_items() {
2908
			$columns               = $this->get_columns(); // Get all necessary column information.
2909
			$hidden                = array(); // No columns to hide, but we must set as an array.
2910
			$sortable              = array(); // No reason to make sortable columns.
2911
			$primary               = $this->get_primary_column_name(); // Column which has the row actions.
2912
			$this->_column_headers = array( $columns, $hidden, $sortable, $primary ); // Get all necessary column headers.
2913
2914
			// Process our bulk activations here.
2915
			if ( 'tgmpa-bulk-activate' === $this->current_action() ) {
2916
				$this->process_bulk_actions();
2917
			}
2918
2919
			// Store all of our plugin data into $items array so WP_List_Table can use it.
2920
			$this->items = apply_filters( 'tgmpa_table_data_items', $this->_gather_plugin_data() );
2921
		}
2922
2923
		/* *********** DEPRECATED METHODS *********** */
2924
2925
		/**
2926
		 * Retrieve plugin data, given the plugin name.
2927
		 *
2928
		 * @since      2.2.0
2929
		 * @deprecated 2.5.0 use {@see TGM_Plugin_Activation::_get_plugin_data_from_name()} instead.
2930
		 * @see        TGM_Plugin_Activation::_get_plugin_data_from_name()
2931
		 *
2932
		 * @param string $name Name of the plugin, as it was registered.
2933
		 * @param string $data Optional. Array key of plugin data to return. Default is slug.
2934
		 * @return string|boolean Plugin slug if found, false otherwise.
2935
		 */
2936
		protected function _get_plugin_data_from_name( $name, $data = 'slug' ) {
2937
			_deprecated_function( __FUNCTION__, 'TGMPA 2.5.0', 'TGM_Plugin_Activation::_get_plugin_data_from_name()' );
2938
2939
			return $this->tgmpa->_get_plugin_data_from_name( $name, $data );
2940
		}
2941
	}
2942
}
2943
2944
2945
if ( ! class_exists( 'TGM_Bulk_Installer' ) ) {
2946
2947
	/**
2948
	 * Hack: Prevent TGMPA v2.4.1- bulk installer class from being loaded if 2.4.1- is loaded after 2.5+.
2949
	 */
2950
	class TGM_Bulk_Installer {
2951
	}
2952
}
2953
if ( ! class_exists( 'TGM_Bulk_Installer_Skin' ) ) {
2954
2955
	/**
2956
	 * Hack: Prevent TGMPA v2.4.1- bulk installer skin class from being loaded if 2.4.1- is loaded after 2.5+.
2957
	 */
2958
	class TGM_Bulk_Installer_Skin {
2959
	}
2960
}
2961
2962
/**
2963
 * The WP_Upgrader file isn't always available. If it isn't available,
2964
 * we load it here.
2965
 *
2966
 * We check to make sure no action or activation keys are set so that WordPress
2967
 * does not try to re-include the class when processing upgrades or installs outside
2968
 * of the class.
2969
 *
2970
 * @since 2.2.0
2971
 */
2972
add_action( 'admin_init', 'tgmpa_load_bulk_installer' );
2973
if ( ! function_exists( 'tgmpa_load_bulk_installer' ) ) {
2974
	/**
2975
	 * Load bulk installer
2976
	 */
2977
	function tgmpa_load_bulk_installer() {
2978
		// Silently fail if 2.5+ is loaded *after* an older version.
2979
		if ( ! isset( $GLOBALS['tgmpa'] ) ) {
2980
			return;
2981
		}
2982
2983
		// Get TGMPA class instance.
2984
		$tgmpa_instance = call_user_func( array( get_class( $GLOBALS['tgmpa'] ), 'get_instance' ) );
2985
2986
		if ( isset( $_GET['page'] ) && $tgmpa_instance->menu === $_GET['page'] ) {
2987
			if ( ! class_exists( 'Plugin_Upgrader', false ) ) {
2988
				require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
2989
			}
2990
2991
			if ( ! class_exists( 'TGMPA_Bulk_Installer' ) ) {
2992
2993
				/**
2994
				 * Installer class to handle bulk plugin installations.
2995
				 *
2996
				 * Extends WP_Upgrader and customizes to suit the installation of multiple
2997
				 * plugins.
2998
				 *
2999
				 * @since 2.2.0
3000
				 *
3001
				 * {@internal Since 2.5.0 the class is an extension of Plugin_Upgrader rather than WP_Upgrader.}}
3002
				 * {@internal Since 2.5.2 the class has been renamed from TGM_Bulk_Installer to TGMPA_Bulk_Installer.
3003
				 *            This was done to prevent backward compatibility issues with v2.3.6.}}
3004
				 *
3005
				 * @package TGM-Plugin-Activation
3006
				 * @author  Thomas Griffin
3007
				 * @author  Gary Jones
3008
				 */
3009
				class TGMPA_Bulk_Installer extends Plugin_Upgrader {
3010
					/**
3011
					 * Holds result of bulk plugin installation.
3012
					 *
3013
					 * @since 2.2.0
3014
					 *
3015
					 * @var string
3016
					 */
3017
					public $result;
3018
3019
					/**
3020
					 * Flag to check if bulk installation is occurring or not.
3021
					 *
3022
					 * @since 2.2.0
3023
					 *
3024
					 * @var boolean
3025
					 */
3026
					public $bulk = false;
3027
3028
					/**
3029
					 * TGMPA instance
3030
					 *
3031
					 * @since 2.5.0
3032
					 *
3033
					 * @var object
3034
					 */
3035
					protected $tgmpa;
3036
3037
					/**
3038
					 * Whether or not the destination directory needs to be cleared ( = on update).
3039
					 *
3040
					 * @since 2.5.0
3041
					 *
3042
					 * @var bool
3043
					 */
3044
					protected $clear_destination = false;
3045
3046
					/**
3047
					 * References parent constructor and sets defaults for class.
3048
					 *
3049
					 * @since 2.2.0
3050
					 *
3051
					 * @param \Bulk_Upgrader_Skin|null $skin Installer skin.
3052
					 */
3053
					public function __construct( $skin = null ) {
3054
						// Get TGMPA class instance.
3055
						$this->tgmpa = call_user_func( array( get_class( $GLOBALS['tgmpa'] ), 'get_instance' ) );
3056
3057
						parent::__construct( $skin );
3058
3059
						if ( isset( $this->skin->options['install_type'] ) && 'update' === $this->skin->options['install_type'] ) {
3060
							$this->clear_destination = true;
3061
						}
3062
3063
						if ( $this->tgmpa->is_automatic ) {
3064
							$this->activate_strings();
3065
						}
3066
3067
						add_action( 'upgrader_process_complete', array( $this->tgmpa, 'populate_file_path' ) );
3068
					}
3069
3070
					/**
3071
					 * Sets the correct activation strings for the installer skin to use.
3072
					 *
3073
					 * @since 2.2.0
3074
					 */
3075
					public function activate_strings() {
3076
						$this->strings['activation_failed']  = __( 'Plugin activation failed.', 'tgmpa' );
3077
						$this->strings['activation_success'] = __( 'Plugin activated successfully.', 'tgmpa' );
3078
					}
3079
3080
					/**
3081
					 * Performs the actual installation of each plugin.
3082
					 *
3083
					 * @since 2.2.0
3084
					 *
3085
					 * @see WP_Upgrader::run()
3086
					 *
3087
					 * @param array $options The installation config options.
3088
					 * @return null|array Return early if error, array of installation data on success.
3089
					 */
3090
					public function run( $options ) {
3091
						$result = parent::run( $options );
3092
3093
						// Reset the strings in case we changed one during automatic activation.
3094
						if ( $this->tgmpa->is_automatic ) {
3095
							if ( 'update' === $this->skin->options['install_type'] ) {
3096
								$this->upgrade_strings();
3097
							} else {
3098
								$this->install_strings();
3099
							}
3100
						}
3101
3102
						return $result;
3103
					}
3104
3105
					/**
3106
					 * Processes the bulk installation of plugins.
3107
					 *
3108
					 * @since 2.2.0
3109
					 *
3110
					 * {@internal This is basically a near identical copy of the WP Core
3111
					 * Plugin_Upgrader::bulk_upgrade() method, with minor adjustments to deal with
3112
					 * new installs instead of upgrades.
3113
					 * For ease of future synchronizations, the adjustments are clearly commented, but no other
3114
					 * comments are added. Code style has been made to comply.}}
3115
					 *
3116
					 * @see Plugin_Upgrader::bulk_upgrade()
3117
					 * @see https://core.trac.wordpress.org/browser/tags/4.2.1/src/wp-admin/includes/class-wp-upgrader.php#L838
3118
					 * (@internal Last synced: Dec 31st 2015 against https://core.trac.wordpress.org/browser/trunk?rev=36134}}
3119
					 *
3120
					 * @param array $plugins The plugin sources needed for installation.
3121
					 * @param array $args    Arbitrary passed extra arguments.
3122
					 * @return string|bool Install confirmation messages on success, false on failure.
3123
					 */
3124
					public function bulk_install( $plugins, $args = array() ) {
3125
						// [TGMPA + ] Hook auto-activation in.
3126
						add_filter( 'upgrader_post_install', array( $this, 'auto_activate' ), 10 );
3127
3128
						$defaults    = array(
3129
							'clear_update_cache' => true,
3130
						);
3131
						$parsed_args = wp_parse_args( $args, $defaults );
3132
3133
						$this->init();
3134
						$this->bulk = true;
3135
3136
						$this->install_strings(); // [TGMPA + ] adjusted.
3137
3138
						/* [TGMPA - ] $current = get_site_transient( 'update_plugins' ); */
3139
3140
						/* [TGMPA - ] add_filter('upgrader_clear_destination', array($this, 'delete_old_plugin'), 10, 4); */
3141
3142
						$this->skin->header();
3143
3144
						// Connect to the Filesystem first.
3145
						$res = $this->fs_connect( array( WP_CONTENT_DIR, WP_PLUGIN_DIR ) );
3146
						if ( ! $res ) {
3147
							$this->skin->footer();
3148
							return false;
3149
						}
3150
3151
						$this->skin->bulk_header();
3152
3153
						/*
3154
						 * Only start maintenance mode if:
3155
						 * - running Multisite and there are one or more plugins specified, OR
3156
						 * - a plugin with an update available is currently active.
3157
						 * @TODO: For multisite, maintenance mode should only kick in for individual sites if at all possible.
3158
						 */
3159
						$maintenance = ( is_multisite() && ! empty( $plugins ) );
3160
3161
						/*
1 ignored issue
show
Unused Code Comprehensibility introduced by
49% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
3162
						[TGMPA - ]
3163
						foreach ( $plugins as $plugin )
3164
							$maintenance = $maintenance || ( is_plugin_active( $plugin ) && isset( $current->response[ $plugin] ) );
3165
						*/
3166
						if ( $maintenance ) {
3167
							$this->maintenance_mode( true );
3168
						}
3169
3170
						$results = array();
3171
3172
						$this->update_count   = count( $plugins );
3173
						$this->update_current = 0;
3174
						foreach ( $plugins as $plugin ) {
3175
							$this->update_current++;
3176
3177
							/*
1 ignored issue
show
Unused Code Comprehensibility introduced by
48% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
3178
							[TGMPA - ]
3179
							$this->skin->plugin_info = get_plugin_data( WP_PLUGIN_DIR . '/' . $plugin, false, true);
3180
3181
							if ( !isset( $current->response[ $plugin ] ) ) {
3182
								$this->skin->set_result('up_to_date');
3183
								$this->skin->before();
3184
								$this->skin->feedback('up_to_date');
3185
								$this->skin->after();
3186
								$results[$plugin] = true;
3187
								continue;
3188
							}
3189
3190
							// Get the URL to the zip file.
3191
							$r = $current->response[ $plugin ];
3192
3193
							$this->skin->plugin_active = is_plugin_active($plugin);
3194
							*/
3195
3196
							$result = $this->run(
3197
								array(
3198
									'package'           => $plugin, // [TGMPA + ] adjusted.
3199
									'destination'       => WP_PLUGIN_DIR,
3200
									'clear_destination' => false, // [TGMPA + ] adjusted.
3201
									'clear_working'     => true,
3202
									'is_multi'          => true,
3203
									'hook_extra'        => array(
3204
										'plugin' => $plugin,
3205
									),
3206
								)
3207
							);
3208
3209
							$results[ $plugin ] = $this->result;
3210
3211
							// Prevent credentials auth screen from displaying multiple times.
3212
							if ( false === $result ) {
3213
								break;
3214
							}
3215
						} //end foreach $plugins
3216
3217
						$this->maintenance_mode( false );
3218
3219
						/**
3220
						 * Fires when the bulk upgrader process is complete.
3221
						 *
3222
						 * @since WP 3.6.0 / TGMPA 2.5.0
3223
						 *
3224
						 * @param Plugin_Upgrader $this Plugin_Upgrader instance. In other contexts, $this, might
3225
						 *                              be a Theme_Upgrader or Core_Upgrade instance.
3226
						 * @param array           $data {
3227
						 *     Array of bulk item update data.
3228
						 *
3229
						 *     @type string $action   Type of action. Default 'update'.
3230
						 *     @type string $type     Type of update process. Accepts 'plugin', 'theme', or 'core'.
3231
						 *     @type bool   $bulk     Whether the update process is a bulk update. Default true.
3232
						 *     @type array  $packages Array of plugin, theme, or core packages to update.
3233
						 * }
3234
						 */
3235
						do_action( 'upgrader_process_complete', $this, array(
3236
							'action'  => 'install', // [TGMPA + ] adjusted.
3237
							'type'    => 'plugin',
3238
							'bulk'    => true,
3239
							'plugins' => $plugins,
3240
						) );
3241
3242
						$this->skin->bulk_footer();
3243
3244
						$this->skin->footer();
3245
3246
						// Cleanup our hooks, in case something else does a upgrade on this connection.
3247
						/* [TGMPA - ] remove_filter('upgrader_clear_destination', array($this, 'delete_old_plugin')); */
3248
3249
						// [TGMPA + ] Remove our auto-activation hook.
3250
						remove_filter( 'upgrader_post_install', array( $this, 'auto_activate' ), 10 );
3251
3252
						// Force refresh of plugin update information.
3253
						wp_clean_plugins_cache( $parsed_args['clear_update_cache'] );
3254
3255
						return $results;
3256
					}
3257
3258
					/**
3259
					 * Handle a bulk upgrade request.
3260
					 *
3261
					 * @since 2.5.0
3262
					 *
3263
					 * @see Plugin_Upgrader::bulk_upgrade()
3264
					 *
3265
					 * @param array $plugins The local WP file_path's of the plugins which should be upgraded.
3266
					 * @param array $args    Arbitrary passed extra arguments.
3267
					 * @return string|bool Install confirmation messages on success, false on failure.
3268
					 */
3269
					public function bulk_upgrade( $plugins, $args = array() ) {
3270
3271
						add_filter( 'upgrader_post_install', array( $this, 'auto_activate' ), 10 );
3272
3273
						$result = parent::bulk_upgrade( $plugins, $args );
3274
3275
						remove_filter( 'upgrader_post_install', array( $this, 'auto_activate' ), 10 );
3276
3277
						return $result;
3278
					}
3279
3280
					/**
3281
					 * Abuse a filter to auto-activate plugins after installation.
3282
					 *
3283
					 * Hooked into the 'upgrader_post_install' filter hook.
3284
					 *
3285
					 * @since 2.5.0
3286
					 *
3287
					 * @param bool $bool The value we need to give back (true).
3288
					 * @return bool
3289
					 */
3290
					public function auto_activate( $bool ) {
3291
						// Only process the activation of installed plugins if the automatic flag is set to true.
3292
						if ( $this->tgmpa->is_automatic ) {
3293
							// Flush plugins cache so the headers of the newly installed plugins will be read correctly.
3294
							wp_clean_plugins_cache();
3295
3296
							// Get the installed plugin file.
3297
							$plugin_info = $this->plugin_info();
3298
3299
							// Don't try to activate on upgrade of active plugin as WP will do this already.
3300
							if ( ! is_plugin_active( $plugin_info ) ) {
3301
								$activate = activate_plugin( $plugin_info );
3302
3303
								// Adjust the success string based on the activation result.
3304
								$this->strings['process_success'] = $this->strings['process_success'] . "<br />\n";
3305
3306
								if ( is_wp_error( $activate ) ) {
3307
									$this->skin->error( $activate );
3308
									$this->strings['process_success'] .= $this->strings['activation_failed'];
3309
								} else {
3310
									$this->strings['process_success'] .= $this->strings['activation_success'];
3311
								}
3312
							}
3313
						}
3314
3315
						return $bool;
3316
					}
3317
				}
3318
			}
3319
3320
			if ( ! class_exists( 'TGMPA_Bulk_Installer_Skin' ) ) {
3321
3322
				/**
3323
				 * Installer skin to set strings for the bulk plugin installations..
3324
				 *
3325
				 * Extends Bulk_Upgrader_Skin and customizes to suit the installation of multiple
3326
				 * plugins.
3327
				 *
3328
				 * @since 2.2.0
3329
				 *
3330
				 * {@internal Since 2.5.2 the class has been renamed from TGM_Bulk_Installer_Skin to
3331
				 *           TGMPA_Bulk_Installer_Skin.
3332
				 *           This was done to prevent backward compatibility issues with v2.3.6.}}
3333
				 *
3334
				 * @see https://core.trac.wordpress.org/browser/trunk/src/wp-admin/includes/class-wp-upgrader-skins.php
3335
				 *
3336
				 * @package TGM-Plugin-Activation
3337
				 * @author  Thomas Griffin
3338
				 * @author  Gary Jones
3339
				 */
3340
				class TGMPA_Bulk_Installer_Skin extends Bulk_Upgrader_Skin {
3341
					/**
3342
					 * Holds plugin info for each individual plugin installation.
3343
					 *
3344
					 * @since 2.2.0
3345
					 *
3346
					 * @var array
3347
					 */
3348
					public $plugin_info = array();
3349
3350
					/**
3351
					 * Holds names of plugins that are undergoing bulk installations.
3352
					 *
3353
					 * @since 2.2.0
3354
					 *
3355
					 * @var array
3356
					 */
3357
					public $plugin_names = array();
3358
3359
					/**
3360
					 * Integer to use for iteration through each plugin installation.
3361
					 *
3362
					 * @since 2.2.0
3363
					 *
3364
					 * @var integer
3365
					 */
3366
					public $i = 0;
3367
3368
					/**
3369
					 * TGMPA instance
3370
					 *
3371
					 * @since 2.5.0
3372
					 *
3373
					 * @var object
3374
					 */
3375
					protected $tgmpa;
3376
3377
					/**
3378
					 * Constructor. Parses default args with new ones and extracts them for use.
3379
					 *
3380
					 * @since 2.2.0
3381
					 *
3382
					 * @param array $args Arguments to pass for use within the class.
3383
					 */
3384
					public function __construct( $args = array() ) {
3385
						// Get TGMPA class instance.
3386
						$this->tgmpa = call_user_func( array( get_class( $GLOBALS['tgmpa'] ), 'get_instance' ) );
3387
3388
						// Parse default and new args.
3389
						$defaults = array(
3390
							'url'          => '',
3391
							'nonce'        => '',
3392
							'names'        => array(),
3393
							'install_type' => 'install',
3394
						);
3395
						$args     = wp_parse_args( $args, $defaults );
3396
3397
						// Set plugin names to $this->plugin_names property.
3398
						$this->plugin_names = $args['names'];
3399
3400
						// Extract the new args.
3401
						parent::__construct( $args );
3402
					}
3403
3404
					/**
3405
					 * Sets install skin strings for each individual plugin.
3406
					 *
3407
					 * Checks to see if the automatic activation flag is set and uses the
3408
					 * the proper strings accordingly.
3409
					 *
3410
					 * @since 2.2.0
3411
					 */
3412
					public function add_strings() {
3413
						if ( 'update' === $this->options['install_type'] ) {
3414
							parent::add_strings();
3415
							$this->upgrader->strings['skin_before_update_header'] = __( 'Updating Plugin %1$s (%2$d/%3$d)', 'tgmpa' );
3416
						} else {
3417
							$this->upgrader->strings['skin_update_failed_error'] = __( 'An error occurred while installing %1$s: <strong>%2$s</strong>.', 'tgmpa' );
3418
							$this->upgrader->strings['skin_update_failed']       = __( 'The installation of %1$s failed.', 'tgmpa' );
3419
3420
							if ( $this->tgmpa->is_automatic ) {
3421
								// Automatic activation strings.
3422
								$this->upgrader->strings['skin_upgrade_start']        = __( 'The installation and activation process is starting. This process may take a while on some hosts, so please be patient.', 'tgmpa' );
3423
								$this->upgrader->strings['skin_update_successful']    = __( '%1$s installed and activated successfully.', 'tgmpa' ) . ' <a href="#" class="hide-if-no-js" onclick="%2$s"><span>' . esc_html__( 'Show Details', 'tgmpa' ) . '</span><span class="hidden">' . esc_html__( 'Hide Details', 'tgmpa' ) . '</span>.</a>';
3424
								$this->upgrader->strings['skin_upgrade_end']          = __( 'All installations and activations have been completed.', 'tgmpa' );
3425
								$this->upgrader->strings['skin_before_update_header'] = __( 'Installing and Activating Plugin %1$s (%2$d/%3$d)', 'tgmpa' );
3426
							} else {
3427
								// Default installation strings.
3428
								$this->upgrader->strings['skin_upgrade_start']        = __( 'The installation process is starting. This process may take a while on some hosts, so please be patient.', 'tgmpa' );
3429
								$this->upgrader->strings['skin_update_successful']    = esc_html__( '%1$s installed successfully.', 'tgmpa' ) . ' <a href="#" class="hide-if-no-js" onclick="%2$s"><span>' . esc_html__( 'Show Details', 'tgmpa' ) . '</span><span class="hidden">' . esc_html__( 'Hide Details', 'tgmpa' ) . '</span>.</a>';
3430
								$this->upgrader->strings['skin_upgrade_end']          = __( 'All installations have been completed.', 'tgmpa' );
3431
								$this->upgrader->strings['skin_before_update_header'] = __( 'Installing Plugin %1$s (%2$d/%3$d)', 'tgmpa' );
3432
							}
3433
						}
3434
					}
3435
3436
					/**
3437
					 * Outputs the header strings and necessary JS before each plugin installation.
3438
					 *
3439
					 * @since 2.2.0
3440
					 *
3441
					 * @param string $title Unused in this implementation.
3442
					 */
3443
					public function before( $title = '' ) {
3444
						if ( empty( $title ) ) {
3445
							$title = esc_html( $this->plugin_names[ $this->i ] );
3446
						}
3447
						parent::before( $title );
3448
					}
3449
3450
					/**
3451
					 * Outputs the footer strings and necessary JS after each plugin installation.
3452
					 *
3453
					 * Checks for any errors and outputs them if they exist, else output
3454
					 * success strings.
3455
					 *
3456
					 * @since 2.2.0
3457
					 *
3458
					 * @param string $title Unused in this implementation.
3459
					 */
3460
					public function after( $title = '' ) {
3461
						if ( empty( $title ) ) {
3462
							$title = esc_html( $this->plugin_names[ $this->i ] );
3463
						}
3464
						parent::after( $title );
3465
3466
						$this->i++;
3467
					}
3468
3469
					/**
3470
					 * Outputs links after bulk plugin installation is complete.
3471
					 *
3472
					 * @since 2.2.0
3473
					 */
3474
					public function bulk_footer() {
3475
						// Serve up the string to say installations (and possibly activations) are complete.
3476
						parent::bulk_footer();
3477
3478
						// Flush plugins cache so we can make sure that the installed plugins list is always up to date.
3479
						wp_clean_plugins_cache();
3480
3481
						$this->tgmpa->show_tgmpa_version();
3482
3483
						// Display message based on if all plugins are now active or not.
3484
						$update_actions = array();
3485
3486
						if ( $this->tgmpa->is_tgmpa_complete() ) {
3487
							// All plugins are active, so we display the complete string and hide the menu to protect users.
3488
							echo '<style type="text/css">#adminmenu .wp-submenu li.current { display: none !important; }</style>';
3489
							$update_actions['dashboard'] = sprintf(
3490
								esc_html( $this->tgmpa->strings['complete'] ),
3491
								'<a href="' . esc_url( self_admin_url() ) . '">' . esc_html__( 'Return to the Dashboard', 'tgmpa' ) . '</a>'
3492
							);
3493
						} else {
3494
							$update_actions['tgmpa_page'] = '<a href="' . esc_url( $this->tgmpa->get_tgmpa_url() ) . '" target="_parent">' . esc_html( $this->tgmpa->strings['return'] ) . '</a>';
3495
						}
3496
3497
						/**
3498
						 * Filter the list of action links available following bulk plugin installs/updates.
3499
						 *
3500
						 * @since 2.5.0
3501
						 *
3502
						 * @param array $update_actions Array of plugin action links.
3503
						 * @param array $plugin_info    Array of information for the last-handled plugin.
3504
						 */
3505
						$update_actions = apply_filters( 'tgmpa_update_bulk_plugins_complete_actions', $update_actions, $this->plugin_info );
3506
3507
						if ( ! empty( $update_actions ) ) {
3508
							$this->feedback( implode( ' | ', (array) $update_actions ) );
3509
						}
3510
					}
3511
3512
					/* *********** DEPRECATED METHODS *********** */
3513
3514
					/**
3515
					 * Flush header output buffer.
3516
					 *
3517
					 * @since      2.2.0
3518
					 * @deprecated 2.5.0 use {@see Bulk_Upgrader_Skin::flush_output()} instead
3519
					 * @see        Bulk_Upgrader_Skin::flush_output()
3520
					 */
3521
					public function before_flush_output() {
3522
						_deprecated_function( __FUNCTION__, 'TGMPA 2.5.0', 'Bulk_Upgrader_Skin::flush_output()' );
3523
						$this->flush_output();
3524
					}
3525
3526
					/**
3527
					 * Flush footer output buffer and iterate $this->i to make sure the
3528
					 * installation strings reference the correct plugin.
3529
					 *
3530
					 * @since      2.2.0
3531
					 * @deprecated 2.5.0 use {@see Bulk_Upgrader_Skin::flush_output()} instead
3532
					 * @see        Bulk_Upgrader_Skin::flush_output()
3533
					 */
3534
					public function after_flush_output() {
3535
						_deprecated_function( __FUNCTION__, 'TGMPA 2.5.0', 'Bulk_Upgrader_Skin::flush_output()' );
3536
						$this->flush_output();
3537
						$this->i++;
3538
					}
3539
				}
3540
			}
3541
		}
3542
	}
3543
}
3544
3545
if ( ! class_exists( 'TGMPA_Utils' ) ) {
3546
3547
	/**
3548
	 * Generic utilities for TGMPA.
3549
	 *
3550
	 * All methods are static, poor-dev name-spacing class wrapper.
3551
	 *
3552
	 * Class was called TGM_Utils in 2.5.0 but renamed TGMPA_Utils in 2.5.1 as this was conflicting with Soliloquy.
3553
	 *
3554
	 * @since 2.5.0
3555
	 *
3556
	 * @package TGM-Plugin-Activation
3557
	 * @author  Juliette Reinders Folmer
3558
	 */
3559
	class TGMPA_Utils {
3560
		/**
3561
		 * Whether the PHP filter extension is enabled.
3562
		 *
3563
		 * @see http://php.net/book.filter
3564
		 *
3565
		 * @since 2.5.0
3566
		 *
3567
		 * @static
3568
		 *
3569
		 * @var bool $has_filters True is the extension is enabled.
3570
		 */
3571
		public static $has_filters;
3572
3573
		/**
3574
		 * Wrap an arbitrary string in <em> tags. Meant to be used in combination with array_map().
3575
		 *
3576
		 * @since 2.5.0
3577
		 *
3578
		 * @static
3579
		 *
3580
		 * @param string $string Text to be wrapped.
3581
		 * @return string
3582
		 */
3583
		public static function wrap_in_em( $string ) {
3584
			return '<em>' . wp_kses_post( $string ) . '</em>';
3585
		}
3586
3587
		/**
3588
		 * Wrap an arbitrary string in <strong> tags. Meant to be used in combination with array_map().
3589
		 *
3590
		 * @since 2.5.0
3591
		 *
3592
		 * @static
3593
		 *
3594
		 * @param string $string Text to be wrapped.
3595
		 * @return string
3596
		 */
3597
		public static function wrap_in_strong( $string ) {
3598
			return '<strong>' . wp_kses_post( $string ) . '</strong>';
3599
		}
3600
3601
		/**
3602
		 * Helper function: Validate a value as boolean
3603
		 *
3604
		 * @since 2.5.0
3605
		 *
3606
		 * @static
3607
		 *
3608
		 * @param mixed $value Arbitrary value.
3609
		 * @return bool
3610
		 */
3611
		public static function validate_bool( $value ) {
3612
			if ( ! isset( self::$has_filters ) ) {
3613
				self::$has_filters = extension_loaded( 'filter' );
3614
			}
3615
3616
			if ( self::$has_filters ) {
3617
				return filter_var( $value, FILTER_VALIDATE_BOOLEAN );
3618
			} else {
3619
				return self::emulate_filter_bool( $value );
3620
			}
3621
		}
3622
3623
		/**
3624
		 * Helper function: Cast a value to bool
3625
		 *
3626
		 * @since 2.5.0
3627
		 *
3628
		 * @static
3629
		 *
3630
		 * @param mixed $value Value to cast.
3631
		 * @return bool
3632
		 */
3633
		protected static function emulate_filter_bool( $value ) {
3634
			// @codingStandardsIgnoreStart
3635
			static $true  = array(
3636
				'1',
3637
				'true', 'True', 'TRUE',
3638
				'y', 'Y',
3639
				'yes', 'Yes', 'YES',
3640
				'on', 'On', 'ON',
3641
			);
3642
			static $false = array(
3643
				'0',
3644
				'false', 'False', 'FALSE',
3645
				'n', 'N',
3646
				'no', 'No', 'NO',
3647
				'off', 'Off', 'OFF',
3648
			);
3649
			// @codingStandardsIgnoreEnd
3650
3651
			if ( is_bool( $value ) ) {
3652
				return $value;
3653
			} else if ( is_int( $value ) && ( 0 === $value || 1 === $value ) ) {
3654
				return (bool) $value;
3655
			} else if ( ( is_float( $value ) && ! is_nan( $value ) ) && ( (float) 0 === $value || (float) 1 === $value ) ) {
3656
				return (bool) $value;
3657
			} else if ( is_string( $value ) ) {
3658
				$value = trim( $value );
3659
				if ( in_array( $value, $true, true ) ) {
3660
					return true;
3661
				} else if ( in_array( $value, $false, true ) ) {
3662
					return false;
3663
				} else {
3664
					return false;
3665
				}
3666
			}
3667
3668
			return false;
3669
		}
3670
	} // End of class TGMPA_Utils
3671
} // End of class_exists wrapper
3672