PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-beta
Jetpack – WP Security, Backup, Speed, & Growth v16.3-beta
16.3 16.3-beta 16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 All 508 releases
← All changes | class.jetpack-modules-list-table.php +647 -0 16.2-beta → 16.3-beta View file →
@@ -1,0 +1,647 @@
1 +<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
2 +/**
3 + * Jetpack modules list table.
4 + *
5 + * @package automattic/jetpack
6 + */
7 +
8 +use Automattic\Jetpack\Assets;
9 +
10 +if ( ! defined( 'ABSPATH' ) ) {
11 + exit( 0 );
12 +}
13 +
14 +if ( ! class_exists( 'WP_List_Table' ) ) {
15 + require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php';
16 +}
17 +
18 +/**
19 + * Jetpack modules list table.
20 + */
21 +class Jetpack_Modules_List_Table extends WP_List_Table {
22 +
23 + /**
24 + * Order in which product groups are displayed on the page.
25 + *
26 + * @var string[]
27 + */
28 + const PRODUCT_GROUP_ORDER = array( 'growth', 'performance', 'security', 'other' );
29 +
30 + /**
31 + * Map of module slug to product group.
32 + *
33 + * Modeled on the Offline Mode dashboard grouping so the modules page reads
34 + * as a set of focused product areas. Any module not listed falls back to
35 + * the "other" group.
36 + *
37 + * @return array<string, string> Module slug => product group slug.
38 + */
39 + public static function get_module_product_groups_map() {
40 + return array(
41 + // Grow your audience.
42 + 'blaze' => 'growth',
43 + 'comment-likes' => 'growth',
44 + 'comments' => 'growth',
45 + 'contact-form' => 'growth',
46 + 'gravatar-hovercards' => 'growth',
47 + 'likes' => 'growth',
48 + 'publicize' => 'growth',
49 + 'related-posts' => 'growth',
50 + 'sharedaddy' => 'growth',
51 + 'simple-payments' => 'growth',
52 + 'stats' => 'growth',
53 + 'subscriptions' => 'growth',
54 + 'woocommerce-analytics' => 'growth',
55 + 'wordads' => 'growth',
56 + 'wpcom-reader' => 'growth',
57 +
58 + // Speed up your site.
59 + 'carousel' => 'performance',
60 + 'google-fonts' => 'performance',
61 + 'infinite-scroll' => 'performance',
62 + 'photon' => 'performance',
63 + 'photon-cdn' => 'performance',
64 + 'search' => 'performance',
65 + 'tiled-gallery' => 'performance',
66 + 'videopress' => 'performance',
67 +
68 + // Protect your site.
69 + 'account-protection' => 'security',
70 + 'monitor' => 'security',
71 + 'notes' => 'security',
72 + 'protect' => 'security',
73 + 'sso' => 'security',
74 + 'vaultpress' => 'security',
75 + 'waf' => 'security',
76 + );
77 + }
78 +
79 + /**
80 + * Get the product group slug for a given module.
81 + *
82 + * @param string $slug Module slug.
83 + * @return string Product group slug.
84 + */
85 + public static function get_module_product_group( $slug ) {
86 + $map = self::get_module_product_groups_map();
87 +
88 + return $map[ $slug ] ?? 'other';
89 + }
90 +
91 + /**
92 + * Get the translated display name for a product group.
93 + *
94 + * @param string $group Product group slug.
95 + * @return string Translated group name.
96 + */
97 + public static function get_product_group_name( $group ) {
98 + switch ( $group ) {
99 + case 'growth':
100 + return __( 'Grow your audience', 'jetpack' );
101 + case 'performance':
102 + return __( 'Speed up your site', 'jetpack' );
103 + case 'security':
104 + return __( 'Protect your site', 'jetpack' );
105 + default:
106 + return __( 'Other features', 'jetpack' );
107 + }
108 + }
109 +
110 + /**
111 + * Whether a module is flagged as recommended.
112 + *
113 + * @param array $module Module data (with raw, untranslated module tags).
114 + * @return bool
115 + */
116 + public static function is_module_recommended( $module ) {
117 + return ! empty( $module['module_tags'] ) && in_array( 'Recommended', (array) $module['module_tags'], true );
118 + }
119 +
120 + /**
121 + * Remove the legacy `vaultpress` module from the modules screen.
122 + *
123 + * The `vaultpress` module is a leftover stub for the original standalone
124 + * VaultPress plugin. It is hard-coded as never activatable (see
125 + * Jetpack_Admin::is_module_available()) and is unrelated to the modern
126 + * VaultPress Backup product, which is delivered through My Jetpack and the
127 + * standalone Jetpack VaultPress Backup plugin rather than a Jetpack module.
128 + * Showing a permanently-unavailable row that merely reuses the modern
129 + * product's name is misleading, so we drop it from this screen only. Other
130 + * surfaces (REST, the React dashboard, standalone plugins) are unaffected.
131 + *
132 + * @param array $modules Array of Jetpack modules keyed by slug.
133 + * @return array Modules without the legacy `vaultpress` entry.
134 + */
135 + public static function hide_legacy_vaultpress_module( $modules ) {
136 + unset( $modules['vaultpress'] );
137 + return $modules;
138 + }
139 +
140 + /** Constructor. */
141 + public function __construct() {
142 + parent::__construct();
143 +
144 + Jetpack::init();
145 +
146 + // Scope this to the modules screen so REST/React/standalone surfaces keep the legacy slug.
147 + add_filter( 'jetpack_modules_list_table_items', array( __CLASS__, 'hide_legacy_vaultpress_module' ) );
148 +
149 + if ( $this->compat_fields && is_array( $this->compat_fields ) ) {
150 + array_push( $this->compat_fields, 'all_items' );
151 + }
152 +
153 + /**
154 + * Filters the list of modules available to be displayed in the Jetpack Settings screen.
155 + *
156 + * @since 3.0.0
157 + *
158 + * @param array $modules Array of Jetpack modules.
159 + */
160 + $this->all_items = apply_filters( 'jetpack_modules_list_table_items', Jetpack_Admin::init()->get_modules() );
161 + $this->items = $this->all_items;
162 + $this->items = $this->filter_displayed_table_items( $this->items );
163 + $this->_column_headers = array( $this->get_columns(), array(), array(), 'name' );
164 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Nonce: This is a view, not a model or controller. InputNotSanitized: Sanitized below via `$this->module_info_check()`.
165 + $modal_info = isset( $_GET['info'] ) ? wp_unslash( $_GET['info'] ) : false;
166 +
167 + // Adding in a hidden h1 heading for screen-readers.
168 + ?>
169 + <h1 class="screen-reader-text"><?php esc_html_e( 'Jetpack Modules List', 'jetpack' ); ?></h1>
170 + <?php
171 +
172 + wp_register_script(
173 + 'models.jetpack-modules',
174 + Assets::get_file_url_for_environment(
175 + '_inc/build/jetpack-modules.models.min.js',
176 + '_inc/jetpack-modules.models.js'
177 + ),
178 + array( 'jquery', 'backbone' ),
179 + JETPACK__VERSION,
180 + false // @todo Can this be put in the footer?
181 + );
182 + wp_register_script(
183 + 'views.jetpack-modules',
184 + Assets::get_file_url_for_environment(
185 + '_inc/build/jetpack-modules.views.min.js',
186 + '_inc/jetpack-modules.views.js'
187 + ),
188 + array( 'jquery', 'backbone', 'wp-util' ),
189 + JETPACK__VERSION,
190 + false // @todo Can this be put in the footer?
191 + );
192 + wp_register_script(
193 + 'jetpack-modules-list-table',
194 + Assets::get_file_url_for_environment(
195 + '_inc/build/jetpack-modules.min.js',
196 + '_inc/jetpack-modules.js'
197 + ),
198 + array(
199 + 'views.jetpack-modules',
200 + 'models.jetpack-modules',
201 + 'jquery',
202 + ),
203 + JETPACK__VERSION,
204 + true
205 + );
206 +
207 + $modules_for_js = (array) Jetpack::get_translated_modules( $this->all_items );
208 + foreach ( array_keys( $modules_for_js ) as $slug ) {
209 + $group = self::get_module_product_group( $slug );
210 +
211 + $modules_for_js[ $slug ]['product_group'] = $group;
212 + $modules_for_js[ $slug ]['product_group_name'] = self::get_product_group_name( $group );
213 + // Detect recommendation from the raw (untranslated) module tags.
214 + $modules_for_js[ $slug ]['recommended'] = isset( $this->all_items[ $slug ] ) && self::is_module_recommended( $this->all_items[ $slug ] );
215 + }
216 +
217 + wp_localize_script(
218 + 'jetpack-modules-list-table',
219 + 'jetpackModulesData',
220 + array(
221 + 'modules' => $modules_for_js,
222 + 'i18n' => array(
223 + 'search_placeholder' => __( 'Search modules…', 'jetpack' ),
224 + ),
225 + 'modalinfo' => $this->module_info_check( $modal_info, $this->all_items ),
226 + 'nonces' => array(
227 + 'bulk' => wp_create_nonce( 'bulk-jetpack_page_jetpack_modules' ),
228 + ),
229 + )
230 + );
231 +
232 + wp_enqueue_script( 'jetpack-modules-list-table' );
233 +
234 + /**
235 + * Filters the js_templates callback value.
236 + *
237 + * @since 3.6.0
238 + *
239 + * @param array array( $this, 'js_templates' ) js_templates callback.
240 + */
241 + add_action( 'admin_footer', apply_filters( 'jetpack_modules_list_table_js_template_callback', array( $this, 'js_templates' ) ), 9 );
242 + }
243 +
244 + /**
245 + * Output row template.
246 + */
247 + public function js_templates() {
248 + ?>
249 + <script type="text/html" id="tmpl-Jetpack_Modules_List_Table_Template">
250 + <# var i = 0; var currentGroup = null;
251 + if ( data.items.length ) {
252 + _.each( data.items, function( item, key, list ) {
253 + if ( item === undefined ) return;
254 + if ( item.product_group !== currentGroup ) {
255 + currentGroup = item.product_group; #>
256 + <tr class="jetpack-module-group-header" data-group="{{ item.product_group }}">
257 + <td colspan="2"><h2 class="jetpack-module-group-header__title">{{ item.product_group_name }}</h2></td>
258 + </tr>
259 + <# } #>
260 + <tr class="jetpack-module<# if ( ++i % 2 ) { #> alternate<# } #><# if ( item.activated ) { #> active<# } #><# if ( ! item.available ) { #> unavailable<# } #>" id="{{{ item.module }}}">
261 + <th scope="row" class="check-column">
262 + <# if ( item.available ) { #><input type="checkbox" name="modules[]" value="{{{ item.module }}}" {{{ item.disabled }}} /><# } #>
263 + </th>
264 + <td class="name column-name">
265 + <div class="jetpack-module__body">
266 + <div class="jetpack-module__info">
267 + <# if ( item.learn_more_button ) { #><a class="jetpack-module__name" href="{{{ item.learn_more_button }}}" target="_blank" rel="noopener noreferrer">{{ item.name }}</a><# } else { #><span class="jetpack-module__name">{{ item.name }}</span><# } #>
268 + <# if ( item.description ) { #><span class="jetpack-module__description">{{ item.description }}</span><# } #>
269 + </div>
270 + <div class="jetpack-module__meta">
271 + <span class="jetpack-module__badges">
272 + <# if ( item.recommended && item.available ) { #><span class="jetpack-module__badge is-recommended"><?php esc_html_e( 'Recommended', 'jetpack' ); ?></span><# } #>
273 + <# if ( item.available ) { #>
274 + <# if ( item.activated ) { #><span class="jetpack-module__badge is-active"><?php esc_html_e( 'Active', 'jetpack' ); ?></span><# } else { #><span class="jetpack-module__badge is-inactive"><?php esc_html_e( 'Inactive', 'jetpack' ); ?></span><# } #>
275 + <# } #>
276 + </span>
277 + <div class="row-actions">
278 + <# if ( item.configurable ) { #>
279 + <span class="configure">{{{ item.configurable }}}</span>
280 + <# } #>
281 + <# if ( item.available ) { #>
282 + <# if ( item.activated ) { #>
283 + <a class="jetpack-module__toggle is-active" role="switch" aria-checked="true" href="<?php echo esc_url( admin_url( 'admin.php' ) ); ?>?page=jetpack&#038;action=deactivate&#038;module={{{ item.module }}}&#038;_wpnonce={{{ item.deactivate_nonce }}}"><span class="jetpack-module__toggle-track"><span class="jetpack-module__toggle-thumb"></span></span><span class="screen-reader-text"><?php esc_html_e( 'Deactivate', 'jetpack' ); ?></span></a>
284 + <# } else { #>
285 + <a class="jetpack-module__toggle" role="switch" aria-checked="false" href="<?php echo esc_url( admin_url( 'admin.php' ) ); ?>?page=jetpack&#038;action=activate&#038;module={{{ item.module }}}&#038;_wpnonce={{{ item.activate_nonce }}}"><span class="jetpack-module__toggle-track"><span class="jetpack-module__toggle-thumb"></span></span><span class="screen-reader-text"><?php esc_html_e( 'Activate', 'jetpack' ); ?></span></a>
286 + <# } #>
287 + <# } else { #>
288 + <span class="jetpack-module__unavailable">{{ item.unavailable_reason }}</span>
289 + <# } #>
290 + </div>
291 + </div>
292 + </div>
293 + </td>
294 + </tr>
295 + <#
296 + });
297 + } else {
298 + #>
299 + <tr class="no-modules-found">
300 + <td colspan="2"><?php esc_html_e( 'No Modules Found', 'jetpack' ); ?></td>
301 + </tr>
302 + <#
303 + }
304 + #>
305 + </script>
306 + <?php
307 + }
308 +
309 + /**
310 + * Get views data.
311 + *
312 + * @return array Maps identifier to display HTML.
313 + */
314 + public function get_views() {
315 + /** This filter is already documented in class.jetpack-modules-list-table.php */
316 + $modules = apply_filters( 'jetpack_modules_list_table_items', Jetpack_Admin::init()->get_modules() );
317 + $array_of_module_tags = wp_list_pluck( $modules, 'module_tags' );
318 + $module_tags = array_merge( ...array_values( $array_of_module_tags ) );
319 + $module_tags = array_map( 'jetpack_get_module_i18n_tag', $module_tags );
320 + $module_tags_unique = array_count_values( $module_tags );
321 + ksort( $module_tags_unique );
322 +
323 + $format = '<a href="%3$s" %4$s data-title="%1$s">%1$s</a> <span class="count">(%2$s)</span>';
324 + $title = __( 'All', 'jetpack' );
325 + $count = is_countable( $modules ) ? count( $modules ) : 0;
326 + $url = esc_url( remove_query_arg( 'module_tag' ) );
327 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This is a view, not a model or controller.
328 + $views = array(
329 + 'all' => sprintf( $format, $title, $count, $url, 'class="all"' ),
330 + );
331 + foreach ( $module_tags_unique as $title => $count ) {
332 + $key = sanitize_title( $title );
333 + $display_title = esc_html( wptexturize( $title ) );
334 + $url = esc_url( add_query_arg( 'module_tag', rawurlencode( $title ) ) );
335 + $views[ $key ] = sprintf( $format, $display_title, $count, $url, '' );
336 + }
337 + return $views;
338 + }
339 +
340 + /**
341 + * Output views HTML.
342 + */
343 + public function views() {
344 + $views = $this->get_views();
345 +
346 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This is a view, not a model or controller.
347 + $module_tag = empty( $_GET['module_tag'] ) ? 'all' : sanitize_title( wp_unslash( $_GET['module_tag'] ) );
348 +
349 + echo "<ul class='subsubsub'>\n";
350 + foreach ( $views as $class => $view ) {
351 + $class_name = $class;
352 + if ( $class === $module_tag ) {
353 + $class_name .= ' current';
354 + }
355 +
356 + $views[ $class ] = "\t<li class='$class_name'>$view</li>";
357 + }
358 + echo implode( "\n", $views ) . "\n"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Is HTML. Escaping happens in get_views().
359 + echo '</ul>';
360 + }
361 +
362 + /**
363 + * Filter a modules array for displayed items.
364 + *
365 + * @param array $modules Modules.
366 + * @return array Displayed modules.
367 + */
368 + public function filter_displayed_table_items( $modules ) {
369 + return array_filter( $modules, array( $this, 'is_module_displayed' ) );
370 + }
371 +
372 + /**
373 + * Determine if a module is displayed.
374 + *
375 + * @param array $module Module data.
376 + * @return bool
377 + */
378 + public static function is_module_displayed( $module ) {
379 + // Handle module tag based filtering.
380 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This is a view, not a model or controller.
381 + if ( ! empty( $_REQUEST['module_tag'] ) ) {
382 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This is a view, not a model or controller.
383 + $module_tag = sanitize_text_field( wp_unslash( $_REQUEST['module_tag'] ) );
384 + if ( ! in_array( $module_tag, array_map( 'jetpack_get_module_i18n_tag', $module['module_tags'] ), true ) ) {
385 + return false;
386 + }
387 + }
388 +
389 + // If nothing rejected it, include it!
390 + return true;
391 + }
392 +
393 + /**
394 + * Sort callback to put modules with `requires_connection` last.
395 + *
396 + * @param array $module1 Module data.
397 + * @param array $module2 Module data.
398 + * @return int Indicating the relative ordering of module1 and module2.
399 + */
400 + public static function sort_requires_connection_last( $module1, $module2 ) {
401 + if ( (bool) $module1['requires_connection'] === (bool) $module2['requires_connection'] ) {
402 + return 0;
403 + }
404 + if ( $module1['requires_connection'] ) {
405 + return 1;
406 + }
407 + if ( $module2['requires_connection'] ) {
408 + return -1;
409 + }
410 +
411 + return 0;
412 + }
413 +
414 + /**
415 + * Get table columns.
416 + *
417 + * @return string[] Column name to header HTML.
418 + */
419 + public function get_columns() {
420 + $columns = array(
421 + 'cb' => '<input type="checkbox" />',
422 + 'name' => __( 'Name', 'jetpack' ),
423 + );
424 + return $columns;
425 + }
426 +
427 + /**
428 + * Get bulk actions for the table.
429 + *
430 + * @return string[] Actions, code => text.
431 + */
432 + public function get_bulk_actions() {
433 + $actions = array(
434 + 'bulk-activate' => __( 'Activate', 'jetpack' ),
435 + 'bulk-deactivate' => __( 'Deactivate', 'jetpack' ),
436 + );
437 + return $actions;
438 + }
439 +
440 + /**
441 + * Print the module rows grouped by product group.
442 + *
443 + * Overrides WP_List_Table::display_rows() so the server-rendered (no-JS)
444 + * output matches the grouped, card-style layout produced by the Backbone
445 + * template.
446 + */
447 + public function display_rows() {
448 + $buckets = array();
449 + foreach ( self::PRODUCT_GROUP_ORDER as $group ) {
450 + $buckets[ $group ] = array();
451 + }
452 +
453 + foreach ( $this->items as $item ) {
454 + $group = self::get_module_product_group( $item['module'] );
455 + $buckets[ $group ][] = $item;
456 + }
457 +
458 + foreach ( $buckets as $group => $items ) {
459 + if ( empty( $items ) ) {
460 + continue;
461 + }
462 + $this->group_header_row( $group );
463 + foreach ( $items as $item ) {
464 + $this->single_row( $item );
465 + }
466 + }
467 + }
468 +
469 + /**
470 + * Print a product group header row.
471 + *
472 + * @param string $group Product group slug.
473 + */
474 + protected function group_header_row( $group ) {
475 + printf(
476 + '<tr class="jetpack-module-group-header" data-group="%1$s"><td colspan="2"><h2 class="jetpack-module-group-header__title">%2$s</h2></td></tr>',
477 + esc_attr( $group ),
478 + esc_html( self::get_product_group_name( $group ) )
479 + );
480 + }
481 +
482 + /**
483 + * Print a single row of the table.
484 + *
485 + * @param object|array $item Item.
486 + */
487 + public function single_row( $item ) {
488 + static $i = 0;
489 + $row_class = ( ( ++$i ) % 2 ) ? ' alternate' : '';
490 +
491 + $available = Jetpack_Admin::is_module_available( $item );
492 +
493 + if ( ! empty( $item['activated'] ) ) {
494 + $row_class .= ' active';
495 + }
496 + if ( ! $available ) {
497 + $row_class .= ' unavailable';
498 + }
499 +
500 + echo '<tr class="jetpack-module' . esc_attr( $row_class ) . '" id="' . esc_attr( $item['module'] ) . '">';
501 +
502 + echo '<th scope="row" class="check-column">';
503 + if ( $available ) {
504 + printf( '<input type="checkbox" name="modules[]" value="%s" />', esc_attr( $item['module'] ) );
505 + }
506 + echo '</th>';
507 +
508 + echo '<td class="name column-name">';
509 + echo '<div class="jetpack-module__body">';
510 +
511 + echo '<div class="jetpack-module__info">';
512 + if ( ! empty( $item['learn_more_button'] ) ) {
513 + printf(
514 + '<a class="jetpack-module__name" href="%1$s" target="_blank" rel="noopener noreferrer">%2$s</a>',
515 + esc_url( $item['learn_more_button'] ),
516 + wp_kses_post( wptexturize( $item['name'] ) )
517 + );
518 + } else {
519 + echo '<span class="jetpack-module__name">' . wp_kses_post( wptexturize( $item['name'] ) ) . '</span>';
520 + }
521 + if ( ! empty( $item['description'] ) ) {
522 + echo '<span class="jetpack-module__description">' . esc_html( wp_strip_all_tags( $item['description'] ) ) . '</span>';
523 + }
524 + echo '</div>';
525 +
526 + echo '<div class="jetpack-module__meta">';
527 + echo '<span class="jetpack-module__badges">';
528 + if ( $available && self::is_module_recommended( $item ) ) {
529 + echo '<span class="jetpack-module__badge is-recommended">' . esc_html__( 'Recommended', 'jetpack' ) . '</span>';
530 + }
531 + if ( $available ) {
532 + if ( ! empty( $item['activated'] ) ) {
533 + echo '<span class="jetpack-module__badge is-active">' . esc_html__( 'Active', 'jetpack' ) . '</span>';
534 + } else {
535 + echo '<span class="jetpack-module__badge is-inactive">' . esc_html__( 'Inactive', 'jetpack' ) . '</span>';
536 + }
537 + }
538 + echo '</span>';
539 +
540 + echo '<div class="row-actions">';
541 + if ( ! empty( $item['configurable'] ) ) {
542 + echo '<span class="configure">' . wp_kses_post( $item['configurable'] ) . '</span>';
543 + }
544 + if ( $available ) {
545 + echo $this->get_module_toggle_html( $item ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped in helper.
546 + } elseif ( ! empty( $item['unavailable_reason'] ) ) {
547 + echo '<span class="jetpack-module__unavailable">' . wp_kses_post( $item['unavailable_reason'] ) . '</span>';
548 + }
549 + // Close .row-actions, .jetpack-module__meta, and .jetpack-module__body.
550 + echo '</div></div></div>';
551 + echo '</td>';
552 + echo '</tr>';
553 + }
554 +
555 + /**
556 + * Build the activate/deactivate toggle control markup for a module row.
557 + *
558 + * @param object|array $item Module data.
559 + * @return string HTML.
560 + */
561 + protected function get_module_toggle_html( $item ) {
562 + $is_active = ! empty( $item['activated'] );
563 +
564 + if ( $is_active ) {
565 + $url = wp_nonce_url(
566 + Jetpack::admin_url(
567 + array(
568 + 'page' => 'jetpack',
569 + 'action' => 'deactivate',
570 + 'module' => $item['module'],
571 + )
572 + ),
573 + 'jetpack_deactivate-' . $item['module']
574 + );
575 + $label = sprintf(
576 + /* translators: %s: Jetpack module name. */
577 + __( 'Deactivate %s', 'jetpack' ),
578 + $item['name']
579 + );
580 + $class = 'jetpack-module__toggle is-active';
581 + $aria = 'true';
582 + } else {
583 + $url = wp_nonce_url(
584 + Jetpack::admin_url(
585 + array(
586 + 'page' => 'jetpack',
587 + 'action' => 'activate',
588 + 'module' => $item['module'],
589 + )
590 + ),
591 + 'jetpack_activate-' . $item['module']
592 + );
593 + $label = sprintf(
594 + /* translators: %s: Jetpack module name. */
595 + __( 'Activate %s', 'jetpack' ),
596 + $item['name']
597 + );
598 + $class = 'jetpack-module__toggle';
599 + $aria = 'false';
600 + }
601 +
602 + return sprintf(
603 + '<a class="%1$s" role="switch" aria-checked="%2$s" href="%3$s"><span class="jetpack-module__toggle-track"><span class="jetpack-module__toggle-thumb"></span></span><span class="screen-reader-text">%4$s</span></a>',
604 + esc_attr( $class ),
605 + esc_attr( $aria ),
606 + esc_url( $url ),
607 + esc_html( $label )
608 + );
609 + }
610 +
611 + /**
612 + * Table classes.
613 + *
614 + * @return string[] HTML.
615 + */
616 + public function get_table_classes() {
617 + return array( 'table', 'table-bordered', 'wp-list-table', 'widefat', 'fixed' );
618 + }
619 +
620 + /**
621 + * Check if the info parameter provided in the URL corresponds to an actual module.
622 + *
623 + * @param string|false $info Info parameter.
624 + * @param array $modules Modules array.
625 + * @return string|false
626 + */
627 + public function module_info_check( $info, $modules ) {
628 + if ( ! $info ) {
629 + return false;
630 + } elseif ( array_key_exists( $info, $modules ) ) {
631 + return $info;
632 + }
633 + }
634 +
635 + /**
636 + * Core switched their `display_tablenav()` method to protected, so we can't access it directly.
637 + * Instead, let's include an access function to make it doable without errors!
638 + *
639 + * @see https://github.com/WordPress/WordPress/commit/d28f6344de97616de8ece543ed290c4ba2383622
640 + *
641 + * @param string $which Which nav table to display.
642 + * @return mixed
643 + */
644 + public function unprotected_display_tablenav( $which = 'top' ) {
645 + return $this->display_tablenav( $which );
646 + }
647 +}