1<?php
2/**
3 * List Table API: WP_MS_Themes_List_Table class
4 *
5 * @package WordPress
6 * @subpackage Administration
7 * @since 3.1.0
8 */
9
10/**
11 * Core class used to implement displaying themes in a list table for the network admin.
12 *
13 * @since 3.1.0
14 *
15 * @see WP_List_Table
16 */
17class WP_MS_Themes_List_Table extends WP_List_Table {
18
19 public $site_id;
20 public $is_site_themes;
21
22 private $has_items;
23
24 /**
25 * Whether to show the auto-updates UI.
26 *
27 * @since 5.5.0
28 *
29 * @var bool True if auto-updates UI is to be shown, false otherwise.
30 */
31 protected $show_autoupdates = true;
32
33 /**
34 * Constructor.
35 *
36 * @since 3.1.0
37 *
38 * @see WP_List_Table::__construct() for more information on default arguments.
39 *
40 * @global string $status
41 * @global int $page
42 *
43 * @param array $args An associative array of arguments.
44 */
45 public function __construct( $args = array() ) {
46 global $status, $page;
47
48 parent::__construct(
49 array(
50 'plural' => 'themes',
51 'screen' => isset( $args['screen'] ) ? $args['screen'] : null,
52 )
53 );
54
55 $status = isset( $_REQUEST['theme_status'] ) ? $_REQUEST['theme_status'] : 'all';
56 if ( ! in_array( $status, array( 'all', 'enabled', 'disabled', 'upgrade', 'search', 'broken', 'auto-update-enabled', 'auto-update-disabled' ), true ) ) {
57 $status = 'all';
58 }
59
60 $page = $this->get_pagenum();
61
62 $this->is_site_themes = ( 'site-themes-network' === $this->screen->id ) ? true : false;
63
64 if ( $this->is_site_themes ) {
65 $this->site_id = isset( $_REQUEST['id'] ) ? (int) $_REQUEST['id'] : 0;
66 }
67
68 $this->show_autoupdates = wp_is_auto_update_enabled_for_type( 'theme' ) &&
69 ! $this->is_site_themes && current_user_can( 'update_themes' );
70 }
71
72 /**
73 * @return array
74 */
75 protected function get_table_classes() {
76 // @todo Remove and add CSS for .themes.
77 return array( 'widefat', 'plugins' );
78 }
79
80 /**
81 * @return bool
82 */
83 public function ajax_user_can() {
84 if ( $this->is_site_themes ) {
85 return current_user_can( 'manage_sites' );
86 } else {
87 return current_user_can( 'manage_network_themes' );
88 }
89 }
90
91 /**
92 * @global string $status
93 * @global array $totals
94 * @global int $page
95 * @global string $orderby
96 * @global string $order
97 * @global string $s
98 */
99 public function prepare_items() {
100 global $status, $totals, $page, $orderby, $order, $s;
101
102 $orderby = ! empty( $_REQUEST['orderby'] ) ? sanitize_text_field( $_REQUEST['orderby'] ) : '';
103 $order = ! empty( $_REQUEST['order'] ) ? sanitize_text_field( $_REQUEST['order'] ) : '';
104 $s = ! empty( $_REQUEST['s'] ) ? sanitize_text_field( $_REQUEST['s'] ) : '';
105
106 $themes = array(
107 /**
108 * Filters the full array of WP_Theme objects to list in the Multisite
109 * themes list table.
110 *
111 * @since 3.1.0
112 *
113 * @param WP_Theme[] $all Array of WP_Theme objects to display in the list table.
114 */
115 'all' => apply_filters( 'all_themes', wp_get_themes() ),
116 'search' => array(),
117 'enabled' => array(),
118 'disabled' => array(),
119 'upgrade' => array(),
120 'broken' => $this->is_site_themes ? array() : wp_get_themes( array( 'errors' => true ) ),
121 );
122
123 if ( $this->show_autoupdates ) {
124 $auto_updates = (array) get_site_option( 'auto_update_themes', array() );
125
126 $themes['auto-update-enabled'] = array();
127 $themes['auto-update-disabled'] = array();
128 }
129
130 if ( $this->is_site_themes ) {
131 $themes_per_page = $this->get_items_per_page( 'site_themes_network_per_page' );
132 $allowed_where = 'site';
133 } else {
134 $themes_per_page = $this->get_items_per_page( 'themes_network_per_page' );
135 $allowed_where = 'network';
136 }
137
138 $current = get_site_transient( 'update_themes' );
139 $maybe_update = current_user_can( 'update_themes' ) && ! $this->is_site_themes && $current;
140
141 foreach ( (array) $themes['all'] as $key => $theme ) {
142 if ( $this->is_site_themes && $theme->is_allowed( 'network' ) ) {
143 unset( $themes['all'][ $key ] );
144 continue;
145 }
146
147 if ( $maybe_update && isset( $current->response[ $key ] ) ) {
148 $themes['all'][ $key ]->update = true;
149 $themes['upgrade'][ $key ] = $themes['all'][ $key ];
150 }
151
152 $filter = $theme->is_allowed( $allowed_where, $this->site_id ) ? 'enabled' : 'disabled';
153 $themes[ $filter ][ $key ] = $themes['all'][ $key ];
154
155 $theme_data = array(
156 'update_supported' => isset( $theme->update_supported ) ? $theme->update_supported : true,
157 );
158
159 // Extra info if known. array_merge() ensures $theme_data has precedence if keys collide.
160 if ( isset( $current->response[ $key ] ) ) {
161 $theme_data = array_merge( (array) $current->response[ $key ], $theme_data );
162 } elseif ( isset( $current->no_update[ $key ] ) ) {
163 $theme_data = array_merge( (array) $current->no_update[ $key ], $theme_data );
164 } else {
165 $theme_data['update_supported'] = false;
166 }
167
168 $theme->update_supported = $theme_data['update_supported'];
169
170 /*
171 * Create the expected payload for the auto_update_theme filter, this is the same data
172 * as contained within $updates or $no_updates but used when the Theme is not known.
173 */
174 $filter_payload = array(
175 'theme' => $key,
176 'new_version' => '',
177 'url' => '',
178 'package' => '',
179 'requires' => '',
180 'requires_php' => '',
181 );
182
183 $filter_payload = (object) array_merge( $filter_payload, array_intersect_key( $theme_data, $filter_payload ) );
184
185 $auto_update_forced = wp_is_auto_update_forced_for_item( 'theme', null, $filter_payload );
186
187 if ( ! is_null( $auto_update_forced ) ) {
188 $theme->auto_update_forced = $auto_update_forced;
189 }
190
191 if ( $this->show_autoupdates ) {
192 $enabled = in_array( $key, $auto_updates, true ) && $theme->update_supported;
193 if ( isset( $theme->auto_update_forced ) ) {
194 $enabled = (bool) $theme->auto_update_forced;
195 }
196
197 if ( $enabled ) {
198 $themes['auto-update-enabled'][ $key ] = $theme;
199 } else {
200 $themes['auto-update-disabled'][ $key ] = $theme;
201 }
202 }
203 }
204
205 if ( $s ) {
206 $status = 'search';
207 $themes['search'] = array_filter( array_merge( $themes['all'], $themes['broken'] ), array( $this, '_search_callback' ) );
208 }
209
210 $totals = array();
211 $js_themes = array();
212 foreach ( $themes as $type => $list ) {
213 $totals[ $type ] = count( $list );
214 $js_themes[ $type ] = array_keys( $list );
215 }
216
217 if ( empty( $themes[ $status ] ) && ! in_array( $status, array( 'all', 'search' ), true ) ) {
218 $status = 'all';
219 }
220
221 $this->items = $themes[ $status ];
222 WP_Theme::sort_by_name( $this->items );
223
224 $this->has_items = ! empty( $themes['all'] );
225 $total_this_page = $totals[ $status ];
226
227 wp_localize_script(
228 'updates',
229 '_wpUpdatesItemCounts',
230 array(
231 'themes' => $js_themes,
232 'totals' => wp_get_update_data(),
233 )
234 );
235
236 if ( $orderby ) {
237 $orderby = ucfirst( $orderby );
238 $order = strtoupper( $order );
239
240 if ( 'Name' === $orderby ) {
241 if ( 'ASC' === $order ) {
242 $this->items = array_reverse( $this->items );
243 }
244 } else {
245 uasort( $this->items, array( $this, '_order_callback' ) );
246 }
247 }
248
249 $start = ( $page - 1 ) * $themes_per_page;
250
251 if ( $total_this_page > $themes_per_page ) {
252 $this->items = array_slice( $this->items, $start, $themes_per_page, true );
253 }
254
255 $this->set_pagination_args(
256 array(
257 'total_items' => $total_this_page,
258 'per_page' => $themes_per_page,
259 )
260 );
261 }
262
263 /**
264 * @param WP_Theme $theme
265 * @return bool
266 */
267 public function _search_callback( $theme ) {
268 static $term = null;
269 if ( is_null( $term ) ) {
270 $term = wp_unslash( $_REQUEST['s'] );
271 }
272
273 foreach ( array( 'Name', 'Description', 'Author', 'Author', 'AuthorURI' ) as $field ) {
274 // Don't mark up; Do translate.
275 if ( false !== stripos( $theme->display( $field, false, true ), $term ) ) {
276 return true;
277 }
278 }
279
280 if ( false !== stripos( $theme->get_stylesheet(), $term ) ) {
281 return true;
282 }
283
284 if ( false !== stripos( $theme->get_template(), $term ) ) {
285 return true;
286 }
287
288 return false;
289 }
290
291 // Not used by any core columns.
292 /**
293 * @global string $orderby
294 * @global string $order
295 * @param array $theme_a
296 * @param array $theme_b
297 * @return int
298 */
299 public function _order_callback( $theme_a, $theme_b ) {
300 global $orderby, $order;
301
302 $a = $theme_a[ $orderby ];
303 $b = $theme_b[ $orderby ];
304
305 if ( $a === $b ) {
306 return 0;
307 }
308
309 if ( 'DESC' === $order ) {
310 return ( $a < $b ) ? 1 : -1;
311 } else {
312 return ( $a < $b ) ? -1 : 1;
313 }
314 }
315
316 /**
317 */
318 public function no_items() {
319 if ( $this->has_items ) {
320 _e( 'No themes found.' );
321 } else {
322 _e( 'No themes are currently available.' );
323 }
324 }
325
326 /**
327 * @return string[] Array of column titles keyed by their column name.
328 */
329 public function get_columns() {
330 $columns = array(
331 'cb' => '<input type="checkbox" />',
332 'name' => __( 'Theme' ),
333 'description' => __( 'Description' ),
334 );
335
336 if ( $this->show_autoupdates ) {
337 $columns['auto-updates'] = __( 'Automatic Updates' );
338 }
339
340 return $columns;
341 }
342
343 /**
344 * @return array
345 */
346 protected function get_sortable_columns() {
347 return array(
348 'name' => array( 'name', false, __( 'Theme' ), __( 'Table ordered by Theme Name.' ), 'asc' ),
349 );
350 }
351
352 /**
353 * Gets the name of the primary column.
354 *
355 * @since 4.3.0
356 *
357 * @return string Unalterable name of the primary column name, in this case, 'name'.
358 */
359 protected function get_primary_column_name() {
360 return 'name';
361 }
362
363 /**
364 * @global array $totals
365 * @global string $status
366 * @return array
367 */
368 protected function get_views() {
369 global $totals, $status;
370
371 $status_links = array();
372 foreach ( $totals as $type => $count ) {
373 if ( ! $count ) {
374 continue;
375 }
376
377 switch ( $type ) {
378 case 'all':
379 /* translators: %s: Number of themes. */
380 $text = _nx(
381 'All <span class="count">(%s)</span>',
382 'All <span class="count">(%s)</span>',
383 $count,
384 'themes'
385 );
386 break;
387 case 'enabled':
388 /* translators: %s: Number of themes. */
389 $text = _nx(
390 'Enabled <span class="count">(%s)</span>',
391 'Enabled <span class="count">(%s)</span>',
392 $count,
393 'themes'
394 );
395 break;
396 case 'disabled':
397 /* translators: %s: Number of themes. */
398 $text = _nx(
399 'Disabled <span class="count">(%s)</span>',
400 'Disabled <span class="count">(%s)</span>',
401 $count,
402 'themes'
403 );
404 break;
405 case 'upgrade':
406 /* translators: %s: Number of themes. */
407 $text = _nx(
408 'Update Available <span class="count">(%s)</span>',
409 'Update Available <span class="count">(%s)</span>',
410 $count,
411 'themes'
412 );
413 break;
414 case 'broken':
415 /* translators: %s: Number of themes. */
416 $text = _nx(
417 'Broken <span class="count">(%s)</span>',
418 'Broken <span class="count">(%s)</span>',
419 $count,
420 'themes'
421 );
422 break;
423 case 'auto-update-enabled':
424 /* translators: %s: Number of themes. */
425 $text = _n(
426 'Auto-updates Enabled <span class="count">(%s)</span>',
427 'Auto-updates Enabled <span class="count">(%s)</span>',
428 $count
429 );
430 break;
431 case 'auto-update-disabled':
432 /* translators: %s: Number of themes. */
433 $text = _n(
434 'Auto-updates Disabled <span class="count">(%s)</span>',
435 'Auto-updates Disabled <span class="count">(%s)</span>',
436 $count
437 );
438 break;
439 }
440
441 if ( $this->is_site_themes ) {
442 $url = 'site-themes.php?id=' . $this->site_id;
443 } else {
444 $url = 'themes.php';
445 }
446
447 if ( 'search' !== $type ) {
448 $status_links[ $type ] = array(
449 'url' => esc_url( add_query_arg( 'theme_status', $type, $url ) ),
450 'label' => sprintf( $text, number_format_i18n( $count ) ),
451 'current' => $type === $status,
452 );
453 }
454 }
455
456 return $this->get_views_links( $status_links );
457 }
458
459 /**
460 * @global string $status
461 *
462 * @return array
463 */
464 protected function get_bulk_actions() {
465 global $status;
466
467 $actions = array();
468 if ( 'enabled' !== $status ) {
469 $actions['enable-selected'] = $this->is_site_themes ? __( 'Enable' ) : __( 'Network Enable' );
470 }
471 if ( 'disabled' !== $status ) {
472 $actions['disable-selected'] = $this->is_site_themes ? __( 'Disable' ) : __( 'Network Disable' );
473 }
474 if ( ! $this->is_site_themes ) {
475 if ( current_user_can( 'update_themes' ) ) {
476 $actions['update-selected'] = __( 'Update' );
477 }
478 if ( current_user_can( 'delete_themes' ) ) {
479 $actions['delete-selected'] = __( 'Delete' );
480 }
481 }
482
483 if ( $this->show_autoupdates ) {
484 if ( 'auto-update-enabled' !== $status ) {
485 $actions['enable-auto-update-selected'] = __( 'Enable Auto-updates' );
486 }
487
488 if ( 'auto-update-disabled' !== $status ) {
489 $actions['disable-auto-update-selected'] = __( 'Disable Auto-updates' );
490 }
491 }
492
493 return $actions;
494 }
495
496 /**
497 * Generates the list table rows.
498 *
499 * @since 3.1.0
500 */
501 public function display_rows() {
502 foreach ( $this->items as $theme ) {
503 $this->single_row( $theme );
504 }
505 }
506
507 /**
508 * Handles the checkbox column output.
509 *
510 * @since 4.3.0
511 * @since 5.9.0 Renamed `$theme` to `$item` to match parent class for PHP 8 named parameter support.
512 *
513 * @param WP_Theme $item The current WP_Theme object.
514 */
515 public function column_cb( $item ) {
516 // Restores the more descriptive, specific name for use within this method.
517 $theme = $item;
518
519 $checkbox_id = 'checkbox_' . md5( $theme->get( 'Name' ) );
520 ?>
521 <input type="checkbox" name="checked[]" value="<?php echo esc_attr( $theme->get_stylesheet() ); ?>" id="<?php echo $checkbox_id; ?>" />
522 <label for="<?php echo $checkbox_id; ?>" >
523 <span class="screen-reader-text">
524 <?php
525 printf(
526 /* translators: Hidden accessibility text. %s: Theme name */
527 __( 'Select %s' ),
528 $theme->display( 'Name' )
529 );
530 ?>
531 </span>
532 </label>
533 <?php
534 }
535
536 /**
537 * Handles the name column output.
538 *
539 * @since 4.3.0
540 *
541 * @global string $status
542 * @global int $page
543 * @global string $s
544 *
545 * @param WP_Theme $theme The current WP_Theme object.
546 */
547 public function column_name( $theme ) {
548 global $status, $page, $s;
549
550 $context = $status;
551
552 if ( $this->is_site_themes ) {
553 $url = "site-themes.php?id={$this->site_id}&";
554 $allowed = $theme->is_allowed( 'site', $this->site_id );
555 } else {
556 $url = 'themes.php?';
557 $allowed = $theme->is_allowed( 'network' );
558 }
559
560 // Pre-order.
561 $actions = array(
562 'enable' => '',
563 'disable' => '',
564 'delete' => '',
565 );
566
567 $stylesheet = $theme->get_stylesheet();
568 $theme_key = urlencode( $stylesheet );
569
570 if ( ! $allowed ) {
571 if ( ! $theme->errors() ) {
572 $url = add_query_arg(
573 array(
574 'action' => 'enable',
575 'theme' => $theme_key,
576 'paged' => $page,
577 's' => $s,
578 ),
579 $url
580 );
581
582 if ( $this->is_site_themes ) {
583 /* translators: %s: Theme name. */
584 $aria_label = sprintf( __( 'Enable %s' ), $theme->display( 'Name' ) );
585 } else {
586 /* translators: %s: Theme name. */
587 $aria_label = sprintf( __( 'Network Enable %s' ), $theme->display( 'Name' ) );
588 }
589
590 $actions['enable'] = sprintf(
591 '<a href="%s" class="edit" aria-label="%s">%s</a>',
592 esc_url( wp_nonce_url( $url, 'enable-theme_' . $stylesheet ) ),
593 esc_attr( $aria_label ),
594 ( $this->is_site_themes ? __( 'Enable' ) : __( 'Network Enable' ) )
595 );
596 }
597 } else {
598 $url = add_query_arg(
599 array(
600 'action' => 'disable',
601 'theme' => $theme_key,
602 'paged' => $page,
603 's' => $s,
604 ),
605 $url
606 );
607
608 if ( $this->is_site_themes ) {
609 /* translators: %s: Theme name. */
610 $aria_label = sprintf( __( 'Disable %s' ), $theme->display( 'Name' ) );
611 } else {
612 /* translators: %s: Theme name. */
613 $aria_label = sprintf( __( 'Network Disable %s' ), $theme->display( 'Name' ) );
614 }
615
616 $actions['disable'] = sprintf(
617 '<a href="%s" aria-label="%s">%s</a>',
618 esc_url( wp_nonce_url( $url, 'disable-theme_' . $stylesheet ) ),
619 esc_attr( $aria_label ),
620 ( $this->is_site_themes ? __( 'Disable' ) : __( 'Network Disable' ) )
621 );
622 }
623
624 if ( ! $allowed && ! $this->is_site_themes
625 && current_user_can( 'delete_themes' )
626 && get_option( 'stylesheet' ) !== $stylesheet
627 && get_option( 'template' ) !== $stylesheet
628 ) {
629 $url = add_query_arg(
630 array(
631 'action' => 'delete-selected',
632 'checked[]' => $theme_key,
633 'theme_status' => $context,
634 'paged' => $page,
635 's' => $s,
636 ),
637 'themes.php'
638 );
639
640 /* translators: %s: Theme name. */
641 $aria_label = sprintf( _x( 'Delete %s', 'theme' ), $theme->display( 'Name' ) );
642
643 $actions['delete'] = sprintf(
644 '<a href="%s" class="delete" aria-label="%s">%s</a>',
645 esc_url( wp_nonce_url( $url, 'bulk-themes' ) ),
646 esc_attr( $aria_label ),
647 __( 'Delete' )
648 );
649 }
650 /**
651 * Filters the action links displayed for each theme in the Multisite
652 * themes list table.
653 *
654 * The action links displayed are determined by the theme's status, and
655 * which Multisite themes list table is being displayed - the Network
656 * themes list table (themes.php), which displays all installed themes,
657 * or the Site themes list table (site-themes.php), which displays the
658 * non-network enabled themes when editing a site in the Network admin.
659 *
660 * The default action links for the Network themes list table include
661 * 'Network Enable', 'Network Disable', and 'Delete'.
662 *
663 * The default action links for the Site themes list table include
664 * 'Enable', and 'Disable'.
665 *
666 * @since 2.8.0
667 *
668 * @param string[] $actions An array of action links.
669 * @param WP_Theme $theme The current WP_Theme object.
670 * @param string $context Status of the theme, one of 'all', 'enabled', or 'disabled'.
671 */
672 $actions = apply_filters( 'theme_action_links', array_filter( $actions ), $theme, $context );
673
674 /**
675 * Filters the action links of a specific theme in the Multisite themes
676 * list table.
677 *
678 * The dynamic portion of the hook name, `$stylesheet`, refers to the
679 * directory name of the theme, which in most cases is synonymous
680 * with the template name.
681 *
682 * @since 3.1.0
683 *
684 * @param string[] $actions An array of action links.
685 * @param WP_Theme $theme The current WP_Theme object.
686 * @param string $context Status of the theme, one of 'all', 'enabled', or 'disabled'.
687 */
688 $actions = apply_filters( "theme_action_links_{$stylesheet}", $actions, $theme, $context );
689
690 echo $this->row_actions( $actions, true );
691 }
692
693 /**
694 * Handles the description column output.
695 *
696 * @since 4.3.0
697 *
698 * @global string $status
699 * @global array $totals
700 *
701 * @param WP_Theme $theme The current WP_Theme object.
702 */
703 public function column_description( $theme ) {
704 global $status, $totals;
705
706 if ( $theme->errors() ) {
707 $pre = 'broken' === $status ? '<strong class="error-message">' . __( 'Broken Theme:' ) . '</strong> ' : '';
708 wp_admin_notice(
709 $pre . $theme->errors()->get_error_message(),
710 array(
711 'type' => 'error',
712 'additional_classes' => 'inline',
713 )
714 );
715 }
716
717 if ( $this->is_site_themes ) {
718 $allowed = $theme->is_allowed( 'site', $this->site_id );
719 } else {
720 $allowed = $theme->is_allowed( 'network' );
721 }
722
723 $class = ! $allowed ? 'inactive' : 'active';
724 if ( ! empty( $totals['upgrade'] ) && ! empty( $theme->update ) ) {
725 $class .= ' update';
726 }
727
728 echo "<div class='theme-description'><p>" . $theme->display( 'Description' ) . "</p></div>
729 <div class='$class second theme-version-author-uri'>";
730
731 $stylesheet = $theme->get_stylesheet();
732 $theme_meta = array();
733
734 if ( $theme->get( 'Version' ) ) {
735 /* translators: %s: Theme version. */
736 $theme_meta[] = sprintf( __( 'Version %s' ), $theme->display( 'Version' ) );
737 }
738
739 /* translators: %s: Theme author. */
740 $theme_meta[] = sprintf( __( 'By %s' ), $theme->display( 'Author' ) );
741
742 if ( $theme->get( 'ThemeURI' ) ) {
743 /* translators: %s: Theme name. */
744 $aria_label = sprintf( __( 'Visit theme site for %s' ), $theme->display( 'Name' ) );
745
746 $theme_meta[] = sprintf(
747 '<a href="%s" aria-label="%s">%s</a>',
748 $theme->display( 'ThemeURI' ),
749 esc_attr( $aria_label ),
750 __( 'Visit Theme Site' )
751 );
752 }
753
754 if ( $theme->parent() ) {
755 $theme_meta[] = sprintf(
756 /* translators: %s: Theme name. */
757 __( 'Child theme of %s' ),
758 '<strong>' . $theme->parent()->display( 'Name' ) . '</strong>'
759 );
760 }
761
762 /**
763 * Filters the array of row meta for each theme in the Multisite themes
764 * list table.
765 *
766 * @since 3.1.0
767 *
768 * @param string[] $theme_meta An array of the theme's metadata, including
769 * the version, author, and theme URI.
770 * @param string $stylesheet Directory name of the theme.
771 * @param WP_Theme $theme WP_Theme object.
772 * @param string $status Status of the theme.
773 */
774 $theme_meta = apply_filters( 'theme_row_meta', $theme_meta, $stylesheet, $theme, $status );
775
776 echo implode( ' | ', $theme_meta );
777
778 echo '</div>';
779 }
780
781 /**
782 * Handles the auto-updates column output.
783 *
784 * @since 5.5.0
785 *
786 * @global string $status
787 * @global int $page
788 *
789 * @param WP_Theme $theme The current WP_Theme object.
790 */
791 public function column_autoupdates( $theme ) {
792 global $status, $page;
793
794 static $auto_updates, $available_updates;
795
796 if ( ! $auto_updates ) {
797 $auto_updates = (array) get_site_option( 'auto_update_themes', array() );
798 }
799 if ( ! $available_updates ) {
800 $available_updates = get_site_transient( 'update_themes' );
801 }
802
803 $stylesheet = $theme->get_stylesheet();
804
805 if ( isset( $theme->auto_update_forced ) ) {
806 if ( $theme->auto_update_forced ) {
807 // Forced on.
808 $text = __( 'Auto-updates enabled' );
809 } else {
810 $text = __( 'Auto-updates disabled' );
811 }
812 $action = 'unavailable';
813 $time_class = ' hidden';
814 } elseif ( empty( $theme->update_supported ) ) {
815 $text = '';
816 $action = 'unavailable';
817 $time_class = ' hidden';
818 } elseif ( in_array( $stylesheet, $auto_updates, true ) ) {
819 $text = __( 'Disable auto-updates' );
820 $action = 'disable';
821 $time_class = '';
822 } else {
823 $text = __( 'Enable auto-updates' );
824 $action = 'enable';
825 $time_class = ' hidden';
826 }
827
828 $query_args = array(
829 'action' => "{$action}-auto-update",
830 'theme' => $stylesheet,
831 'paged' => $page,
832 'theme_status' => $status,
833 );
834
835 $url = add_query_arg( $query_args, 'themes.php' );
836
837 if ( 'unavailable' === $action ) {
838 $html[] = '<span class="label">' . $text . '</span>';
839 } else {
840 $html[] = sprintf(
841 '<a href="%s" class="toggle-auto-update aria-button-if-js" data-wp-action="%s">',
842 wp_nonce_url( $url, 'updates' ),
843 $action
844 );
845
846 $html[] = '<span class="dashicons dashicons-update spin hidden" aria-hidden="true"></span>';
847 $html[] = '<span class="label">' . $text . '</span>';
848 $html[] = '</a>';
849
850 }
851
852 if ( isset( $available_updates->response[ $stylesheet ] ) ) {
853 $html[] = sprintf(
854 '<div class="auto-update-time%s">%s</div>',
855 $time_class,
856 wp_get_auto_update_message()
857 );
858 }
859
860 $html = implode( '', $html );
861
862 /**
863 * Filters the HTML of the auto-updates setting for each theme in the Themes list table.
864 *
865 * @since 5.5.0
866 *
867 * @param string $html The HTML for theme's auto-update setting, including
868 * toggle auto-update action link and time to next update.
869 * @param string $stylesheet Directory name of the theme.
870 * @param WP_Theme $theme WP_Theme object.
871 */
872 echo apply_filters( 'theme_auto_update_setting_html', $html, $stylesheet, $theme );
873
874 wp_admin_notice(
875 '',
876 array(
877 'type' => 'error',
878 'additional_classes' => array( 'notice-alt', 'inline', 'hidden' ),
879 )
880 );
881 }
882
883 /**
884 * Handles default column output.
885 *
886 * @since 4.3.0
887 * @since 5.9.0 Renamed `$theme` to `$item` to match parent class for PHP 8 named parameter support.
888 *
889 * @param WP_Theme $item The current WP_Theme object.
890 * @param string $column_name The current column name.
891 */
892 public function column_default( $item, $column_name ) {
893 // Restores the more descriptive, specific name for use within this method.
894 $theme = $item;
895
896 $stylesheet = $theme->get_stylesheet();
897
898 /**
899 * Fires inside each custom column of the Multisite themes list table.
900 *
901 * @since 3.1.0
902 *
903 * @param string $column_name Name of the column.
904 * @param string $stylesheet Directory name of the theme.
905 * @param WP_Theme $theme Current WP_Theme object.
906 */
907 do_action( 'manage_themes_custom_column', $column_name, $stylesheet, $theme );
908 }
909
910 /**
911 * Handles the output for a single table row.
912 *
913 * @since 4.3.0
914 *
915 * @param WP_Theme $item The current WP_Theme object.
916 */
917 public function single_row_columns( $item ) {
918 list( $columns, $hidden, $sortable, $primary ) = $this->get_column_info();
919
920 foreach ( $columns as $column_name => $column_display_name ) {
921 $extra_classes = '';
922 if ( in_array( $column_name, $hidden, true ) ) {
923 $extra_classes .= ' hidden';
924 }
925
926 switch ( $column_name ) {
927 case 'cb':
928 echo '<th scope="row" class="check-column">';
929
930 $this->column_cb( $item );
931
932 echo '</th>';
933 break;
934
935 case 'name':
936 $active_theme_label = '';
937
938 /* The presence of the site_id property means that this is a subsite view and a label for the active theme needs to be added */
939 if ( ! empty( $this->site_id ) ) {
940 $stylesheet = get_blog_option( $this->site_id, 'stylesheet' );
941 $template = get_blog_option( $this->site_id, 'template' );
942
943 /* Add a label for the active template */
944 if ( $item->get_template() === $template ) {
945 $active_theme_label = ' — ' . __( 'Active Theme' );
946 }
947
948 /* In case this is a child theme, label it properly */
949 if ( $stylesheet !== $template && $item->get_stylesheet() === $stylesheet ) {
950 $active_theme_label = ' — ' . __( 'Active Child Theme' );
951 }
952 }
953
954 echo "<td class='theme-title column-primary{$extra_classes}'><strong>" . $item->display( 'Name' ) . $active_theme_label . '</strong>';
955
956 $this->column_name( $item );
957
958 echo '</td>';
959 break;
960
961 case 'description':
962 echo "<td class='column-description desc{$extra_classes}'>";
963
964 $this->column_description( $item );
965
966 echo '</td>';
967 break;
968
969 case 'auto-updates':
970 echo "<td class='column-auto-updates{$extra_classes}'>";
971
972 $this->column_autoupdates( $item );
973
974 echo '</td>';
975 break;
976 default:
977 echo "<td class='$column_name column-$column_name{$extra_classes}'>";
978
979 $this->column_default( $item, $column_name );
980
981 echo '</td>';
982 break;
983 }
984 }
985 }
986
987 /**
988 * @global string $status
989 * @global array $totals
990 *
991 * @param WP_Theme $theme
992 */
993 public function single_row( $theme ) {
994 global $status, $totals;
995
996 if ( $this->is_site_themes ) {
997 $allowed = $theme->is_allowed( 'site', $this->site_id );
998 } else {
999 $allowed = $theme->is_allowed( 'network' );
1000 }
1001
1002 $stylesheet = $theme->get_stylesheet();
1003
1004 $class = ! $allowed ? 'inactive' : 'active';
1005 if ( ! empty( $totals['upgrade'] ) && ! empty( $theme->update ) ) {
1006 $class .= ' update';
1007 }
1008
1009 printf(
1010 '<tr class="%s" data-slug="%s">',
1011 esc_attr( $class ),
1012 esc_attr( $stylesheet )
1013 );
1014
1015 $this->single_row_columns( $theme );
1016
1017 echo '</tr>';
1018
1019 if ( $this->is_site_themes ) {
1020 remove_action( "after_theme_row_$stylesheet", 'wp_theme_update_row' );
1021 }
1022
1023 /**
1024 * Fires after each row in the Multisite themes list table.
1025 *
1026 * @since 3.1.0
1027 *
1028 * @param string $stylesheet Directory name of the theme.
1029 * @param WP_Theme $theme Current WP_Theme object.
1030 * @param string $status Status of the theme.
1031 */
1032 do_action( 'after_theme_row', $stylesheet, $theme, $status );
1033
1034 /**
1035 * Fires after each specific row in the Multisite themes list table.
1036 *
1037 * The dynamic portion of the hook name, `$stylesheet`, refers to the
1038 * directory name of the theme, most often synonymous with the template
1039 * name of the theme.
1040 *
1041 * @since 3.5.0
1042 *
1043 * @param string $stylesheet Directory name of the theme.
1044 * @param WP_Theme $theme Current WP_Theme object.
1045 * @param string $status Status of the theme.
1046 */
1047 do_action( "after_theme_row_{$stylesheet}", $stylesheet, $theme, $status );
1048 }
1049}
1050