PluginProbe
MainWP Dashboard: Self-hosted WordPress Management for Agencies / trunk
MainWP Dashboard: Self-hosted WordPress Management for Agencies vtrunk
6.2 6.1.8 6.1.7 6.1.6 6.1.5 6.1.4 6.1.3 6.1.2 6.1.1 6.1 6.0.12 6.0.11 4.6.0.1 5.0 5.0.1 5.0.2 5.0.3 5.0.3.1 5.0.3.2 5.1 5.1.1 5.2 5.2.1 5.2.2 5.3 All 153 releases
mainwp / class / class-mainwp-plugins-install-list-table.php

class-mainwp-plugins-install-list-table.php in MainWP Dashboard: Self-hosted WordPress Management for Agencies trunk, at class/class-mainwp-plugins-install-list-table.php

665 lines 26.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Plugin Installer List Table class.
4 *
5 * @package MainWP/Dashboard
6 */
7
8 namespace MainWP\Dashboard;
9
10 // Exit if accessed directly.
11 if ( ! defined( 'ABSPATH' ) ) {
12 exit;
13 }
14
15 // Include class-wp-list-table.php.
16 if ( ! class_exists( '\WP_List_Table' ) ) {
17 require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php'; // NOSONAR - WP compatible.
18 }
19
20 /**
21 * Class MainWP_Plugins_Install_List_Table
22 *
23 * @package MainWP\Dashboard
24 * @subpackage List_Table
25 * @since 3.1.0
26 * @access private
27 */
28 class MainWP_Plugins_Install_List_Table extends \WP_List_Table { // phpcs:ignore Generic.Classes.OpeningBraceSameLine.ContentAfterBrace -- NOSONAR.
29
30 /**
31 * Default direction.
32 *
33 * @var string $order
34 */
35 public $order = 'ASC';
36
37 /**
38 * Default order.
39 *
40 * @var int $orderby
41 */
42 public $orderby = null;
43
44 /**
45 * Groups array.
46 *
47 * @var array $groups
48 */
49 public $groups = array();
50
51 /**
52 * Error messages.
53 *
54 * @var mixed $error
55 */
56 private $error;
57
58 /**
59 * Method ajax_user_can()
60 *
61 * Chck if the current user has the WP ability "install_plugins".
62 *
63 * @return boolean True|False.
64 */
65 public function ajax_user_can() {
66 return current_user_can( 'install_plugins' );
67 }
68
69 /**
70 * Method prepair_items()
71 *
72 * @global array $tabs
73 * @global string $tab
74 * @global int $paged
75 * @global string $type
76 * @global string $term
77 * @global string $wp_version
78 */
79 public function prepare_items() { // phpcs:ignore -- NOSONAR -Current complexity is the only way to achieve desired results, pull request solutions appreciated.
80 include_once ABSPATH . 'wp-admin/includes/plugin-install.php'; // NOSONAR - WP compatible.
81
82 global $tab; // required.
83
84 wp_reset_vars( array( 'tab' ) );
85
86 $paged = $this->get_pagenum();
87
88 $per_page = 40;
89
90 // These are the tabs which are shown on the page.
91 $tabs = array();
92
93 if ( 'search' === $tab ) {
94 $tabs['search'] = esc_html__( 'Search Results', 'mainwp' );
95 }
96 $tabs['featured'] = _x( 'Featured', 'Plugin Installer', 'mainwp' );
97 $tabs['popular'] = _x( 'Popular', 'Plugin Installer', 'mainwp' );
98 $tabs['recommended'] = _x( 'Recommended', 'Plugin Installer', 'mainwp' );
99 if ( 'beta' === $tab || false !== strpos( $GLOBALS['wp_version'], '-' ) ) {
100 $tabs['beta'] = _x( 'Beta Testing', 'Plugin Installer', 'mainwp' );
101 }
102 if ( current_user_can( 'upload_plugins' ) ) {
103 // No longer a real tab. Here for filter compatibility.
104 // Gets skipped in get_views().
105 $tabs['upload'] = esc_html__( 'Upload Plugin', 'mainwp' );
106 }
107
108 $nonmenu_tabs = array( 'plugin-information' );
109 // Valid actions to perform which do not have a Menu item.
110 // If a non-valid menu tab has been selected, And it's not a non-menu action.
111 if ( empty( $tab ) || ( ! isset( $tabs[ $tab ] ) && ! in_array( $tab, (array) $nonmenu_tabs ) ) ) {
112
113 $tab = key( $tabs ); // phpcs:ignore -- required for custom bulk install plugins.
114 }
115
116 $args = array(
117 'page' => $paged,
118 'per_page' => $per_page,
119 'fields' => array(
120 'last_updated' => true,
121 'icons' => true,
122 'active_installs' => true,
123 ),
124 'locale' => get_locale(),
125 'installed_plugins' => array(),
126 );
127
128 switch ( $tab ) {
129 case 'search':
130 $type = isset( $_REQUEST['type'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['type'] ) ) : 'term'; // phpcs:ignore WordPress.Security.NonceVerification,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
131 $term = isset( $_REQUEST['s'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['s'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
132
133 switch ( $type ) {
134 case 'tag':
135 $args['tag'] = sanitize_title_with_dashes( $term );
136 break;
137 case 'term':
138 $args['search'] = $term;
139 break;
140 case 'author':
141 $args['author'] = $term;
142 break;
143 default:
144 break;
145 }
146
147 break;
148
149 case 'featured':
150 $args['fields']['group'] = true;
151 $this->orderby = 'group';
152 // no b r e a k!
153 case 'popular':
154 case 'new':
155 case 'beta':
156 case 'recommended':
157 $args['browse'] = $tab;
158 break;
159
160 case 'favorites':
161 $user = isset( $_GET['user'] ) ? sanitize_text_field( wp_unslash( $_GET['user'] ) ) : get_user_option( 'wporg_favorites' ); // phpcs:ignore WordPress.Security.NonceVerification,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
162 update_user_meta( get_current_user_id(), 'wporg_favorites', $user );
163 if ( $user ) {
164 $args['user'] = $user;
165 } else {
166 $args = false;
167 }
168
169 break;
170
171 default:
172 $args = false;
173 break;
174 }
175
176 if ( ! $args ) {
177 return;
178 }
179
180 $api = plugins_api( 'query_plugins', $args );
181
182 if ( is_wp_error( $api ) ) {
183 $this->error = $api;
184 return;
185 }
186
187 $this->items = $api->plugins;
188
189 if ( $this->orderby ) {
190 uasort( $this->items, array( $this, 'order_callback' ) );
191 }
192
193 $this->set_pagination_args(
194 array(
195 'total_items' => $api->info['results'],
196 'per_page' => $args['per_page'],
197 )
198 );
199
200 if ( isset( $api->info['groups'] ) ) {
201 $this->groups = $api->info['groups'];
202 }
203 }
204
205 /**
206 * Method no_items()
207 *
208 * Check for errors.
209 */
210 public function no_items() {
211 echo '<div class="ui center aligned segment">';
212 MainWP_UI::render_empty_page_placeholder( __( 'No plugins found.', 'mainwp' ), __( 'Please try a different search.', 'mainwp' ), '<em data-emoji=":mag:" class="big"></em>' );
213 echo '</div>';
214 }
215
216 /**
217 * Method dislpay()
218 *
219 * Override the parent display() so we can provide a different container.
220 */
221 public function display() {
222 $search = isset( $_GET['s'] ) ? sanitize_text_field( wp_unslash( $_GET['s'] ) ) : '';
223 ?>
224 <div class="ui two column stackable grid">
225 <div class="column left aligned middle aligned">
226 <?php if ( ! empty( $search ) ) : ?>
227 <h3 class="ui header"><?php
228 /* translators: %s: Search term. */
229 echo esc_html( sprintf( __( 'Search results for: %s', 'mainwp' ), $search ) ); ?></h3>
230 <?php endif; ?>
231 </div>
232 <div class="column right aligned middle aligned">
233 <div class="inline field">
234 <?php $this->display_tablenav( 'top' ); ?>
235 </div>
236 </div>
237 </div>
238 <div class="ui hidden divider"></div>
239 <div id="mainwp-install-plugins-container" class="ui three mainwp-cards cards">
240 <?php $this->display_rows_or_placeholder(); ?>
241 </div>
242 <div class="ui hidden divider"></div>
243 <div class="ui two column stackable grid">
244 <div class="column left aligned middle aligned">
245 <?php if ( ! empty( $search ) ) : ?>
246 <?php
247 /* translators: %s: Search term. */
248 echo esc_html( sprintf( __( 'Search results for: %s', 'mainwp' ), $search ) ); ?>
249 <?php endif; ?>
250 </div>
251 <div class="column right aligned middle aligned">
252 <div class="inline field">
253 <?php $this->display_tablenav( 'bottom' ); ?>
254 </div>
255 </div>
256 </div>
257 <?php
258 }
259
260 /**
261 * Method display_tablenav()
262 *
263 * Displays the table Navigation.
264 *
265 * @param mixed $which Position information.
266 *
267 * @return mixed wp_referer_field();
268 */
269 protected function display_tablenav( $which ) {
270
271 if ( 'featured' === $GLOBALS['tab'] ) {
272 return;
273 }
274
275 if ( 'top' === $which ) {
276 wp_referer_field();
277 $this->pagination( $which );
278 } else {
279 $this->pagination( $which );
280 }
281 }
282
283 /**
284 * Method pagination()
285 *
286 * Build the pagination menu.
287 *
288 * @param mixed $which Position information.
289 *
290 * @return mixed Pagination HTML
291 */
292 protected function pagination( $which ) { //phpcs:ignore -- NOSONAR - complex.
293 if ( empty( $this->_pagination_args ) ) {
294 return;
295 }
296
297 $total_items = $this->_pagination_args['total_items'];
298 $total_pages = (int) $this->_pagination_args['total_pages'];
299
300 /* translators: %s: Number of items. */
301 $perpage_paging = '<span>' . sprintf( _n( '%s item', '%s items', $total_items, 'mainwp' ), number_format_i18n( $total_items ) ) . '</span>';
302
303 $current = (int) $this->get_pagenum();
304 $removable_query_args = wp_removable_query_args();
305
306 $current_url = set_url_scheme( 'http://' . ( isset( $_SERVER['HTTP_HOST'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) ) : '' ) . ( isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '' ) ); // phpcs:ignore WordPress.Security.NonceVerification -- NOSONAR - compatible with dashboard host.
307
308 $current_url = remove_query_arg( $removable_query_args, $current_url );
309
310 $page_links = array();
311
312 $disable_first = false;
313 $disable_last = false;
314 $disable_prev = false;
315 $disable_next = false;
316
317 if ( 1 === $current ) {
318 $disable_first = true;
319 $disable_prev = true;
320 }
321 if ( 2 === $current ) {
322 $disable_first = true;
323 }
324 if ( $current === $total_pages ) {
325 $disable_last = true;
326 $disable_next = true;
327 }
328 if ( $current === $total_pages - 1 ) {
329 $disable_last = true;
330 }
331
332 if ( $disable_first ) {
333 $page_links[] = '<a class="item disabled" aria-hidden="true"><i class="angle double left icon"></i></a>';
334 } else {
335 $page_links[] = sprintf( "<a class='item' href='%s' title='" . esc_html__( 'First page', 'mainwp' ) . "' aria-hidden='true'>%s</a>", esc_url( remove_query_arg( 'paged', $current_url ) ), '<i class="angle double left icon"></i>' );
336 }
337
338 if ( $disable_prev ) {
339 $page_links[] = '<a class="item disabled" aria-hidden="true"><i class="angle left icon"></i></a>';
340 } else {
341 $page_links[] = sprintf( "<a class='item' href='%s' title='" . esc_html__( 'Previous page', 'mainwp' ) . "' aria-hidden='true'>%s</a>", esc_url( add_query_arg( 'paged', max( 1, $current - 1 ), $current_url ) ), '<i class="angle left icon"></i>' );
342 }
343
344 if ( $current - 1 > 0 ) {
345 $page_links[] = sprintf( "<a class='item' href='%s'>%s</a>", esc_url( add_query_arg( 'paged', $current - 1, $current_url ) ), $current - 1 );
346 }
347
348 $page_links[] = sprintf( "<a class='item active' href='%s'>%s</a>", esc_url( add_query_arg( 'paged', $current, $current_url ) ), $current );
349
350 if ( $current + 1 <= $total_pages ) {
351 $page_links[] = sprintf( "<a class='item' href='%s'>%s</a>", esc_url( add_query_arg( 'paged', $current + 1, $current_url ) ), $current + 1 );
352 }
353
354 if ( $disable_next ) {
355 $page_links[] = '<span class="item disabled " aria-hidden="true"><i class="right angle icon"></i></span>';
356 } else {
357 $page_links[] = sprintf( "<a class='item' href='%s' title='" . esc_html__( 'Next page', 'mainwp' ) . "'>%s</a>", esc_url( add_query_arg( 'paged', min( $total_pages, $current + 1 ), $current_url ) ), '<i class="angle right icon"></i>' );
358 }
359
360 if ( $disable_last ) {
361 $page_links[] = '<a class="item disabled" aria-hidden="true"><i class="right angle double icon"></i></a>';
362 } else {
363 $page_links[] = sprintf( "<a class='item' href='%s' title='" . esc_html__( 'Last page', 'mainwp' ) . "' aria-hidden='true'>%s</a>", esc_url( add_query_arg( 'paged', $total_pages, $current_url ) ), '<i class="right angle double icon"></i>' );
364 }
365
366 if ( $total_pages > 1 ) {
367 $perpage_paging = $perpage_paging . "&nbsp;&nbsp;<div class='ui mini pagination menu'>" . join( "\n", $page_links ) . '</div>';
368 }
369
370 ob_start();
371
372 echo $perpage_paging; // phpcs:ignore WordPress.Security.EscapeOutput
373
374 $output = ob_get_clean();
375
376 $this->_pagination = $output;
377
378 echo $this->_pagination; // phpcs:ignore WordPress.Security.EscapeOutput
379 }
380
381 /**
382 * Method get_columns()
383 *
384 * Get the collumns to display.
385 *
386 * @return array List of collumns to display.
387 */
388 public function get_columns() {
389 return array();
390 }
391
392 /**
393 * Method order_callback()
394 *
395 * @param object $plugin_a Plugin A.
396 * @param object $plugin_b Plugin B.
397 *
398 * @return int 0|1 Default 0.
399 */
400 private function order_callback( $plugin_a, $plugin_b ) {
401 $orderby = $this->orderby;
402 if ( ! isset( $plugin_a->$orderby, $plugin_b->$orderby ) ) {
403 return 0;
404 }
405
406 $a = $plugin_a->$orderby;
407 $b = $plugin_b->$orderby;
408
409 if ( $a === $b ) {
410 return 0;
411 }
412
413 if ( 'DESC' === $this->order ) {
414 return ( $a < $b ) ? 1 : -1;
415 } else {
416 return ( $a < $b ) ? -1 : 1;
417 }
418 }
419
420 /**
421 * Check if a search term matches mainwp or mainwp child (case-insensitive).
422 *
423 * @param string $term Search term.
424 * @return bool True if matches.
425 */
426 private function is_mainwp_search( $term ) {
427 $normalized = strtolower( trim( $term ) );
428 return (bool) preg_match( '/^mainwp(\s*-?\s*child)?$/i', $normalized );
429 }
430
431 /**
432 * Render hardcoded MainWP Child plugin card.
433 *
434 * @return void
435 */
436 private function render_mainwp_child_card() {
437 $details_link = self_admin_url( 'plugin-install.php?tab=plugin-information&plugin=mainwp-child&name=' . rawurlencode( 'MainWP Child' ) );
438 $icon_url = MAINWP_PLUGIN_URL . 'assets/images/mainwp-icon.svg';
439 $plugin_name = esc_attr__( 'MainWP Child &#8211; Securely Connects to the MainWP Dashboard to Manage Multiple Sites', 'mainwp' );
440 ?>
441 <div class="card plugin-card-mainwp-child" plugin-slug="mainwp-child" plugin-name="MainWP Child">
442 <?php do_action( 'mainwp_install_plugin_card_top' ); ?>
443 <div class="content">
444 <a class="right floated mini ui image open-plugin-details-modal" href="<?php echo esc_url( $details_link ); ?>"><img src="<?php echo esc_url( $icon_url ); ?>" alt="<?php esc_attr_e( 'MainWP Child', 'mainwp' ); ?>" /></a>
445 <div class="header">
446 <a class="open-plugin-details-modal" href="<?php echo esc_url( $details_link ); ?>"><?php esc_html_e( 'MainWP Child', 'mainwp' ); ?></a>
447 </div>
448 <div class="meta"><cite><?php
449 /* translators: %s: Plugin author name. */
450 printf( esc_html__( 'By %s', 'mainwp' ), '<a href="' . esc_url( 'https://profiles.wordpress.org/mainwp/' ) . '">mainwp</a>' ); // phpcs:ignore WordPress.Security.EscapeOutput ?></cite></div>
451 <div class="description">
452 <p><?php esc_html_e( 'MainWP Child connects your WordPress sites to this Dashboard.', 'mainwp' ); ?></p>
453 <p><?php esc_html_e( 'Already managing a site here? Select Install Plugin, choose your sites in the right panel, and click Complete Installation.', 'mainwp' ); ?></p>
454 <p><?php esc_html_e( 'Setting up a new site? Download the plugin, then:', 'mainwp' ); ?></p>
455 <div class="ui list">
456 <div class="item"><p>1. <?php esc_html_e( 'Log in to your WordPress site', 'mainwp' ); ?></p></div>
457 <div class="item"><p>2. <?php esc_html_e( 'Go to Plugins → Add New → Upload Plugin', 'mainwp' ); ?></p></div>
458 <div class="item"><p>3. <?php esc_html_e( 'Upload and activate the downloaded file', 'mainwp' ); ?></p></div>
459 <div class="item"><p>4. <?php esc_html_e( 'Return here to add the site to your Dashboard', 'mainwp' ); ?></p></div>
460 </div>
461 </div>
462 </div>
463 <div class="extra content">
464 <div class="left aligned middle aligned column">
465 <a href="<?php echo esc_url( 'https://downloads.wordpress.org/plugin/mainwp-child.latest-stable.zip' ); ?>" class="ui mini green basic fluid button"><i class="download icon"></i><?php esc_html_e( 'Download', 'mainwp' ); ?></a>
466 </div>
467 </div>
468 <div class="extra content">
469 <div class="ui two column grid">
470 <div class="left aligned middle aligned column">
471 <a href="<?php echo esc_url( $details_link ); ?>" class="ui mini button open-plugin-details-modal"><?php esc_html_e( 'Plugin Details', 'mainwp' ); ?></a>
472 </div>
473 <div class="right aligned middle aligned column">
474 <div class="ui radio checkbox">
475 <input name="install-plugin" type="radio" id="install-plugin-mainwp-child" plugin-name="<?php echo esc_attr( $plugin_name ); ?>" plugin-version="">
476 <label><?php esc_html_e( 'Install Plugin', 'mainwp' ); ?></label>
477 </div>
478 </div>
479 </div>
480 </div>
481 </div>
482 <?php
483 }
484
485 /**
486 * Method display_rows()
487 *
488 * Build Plugin Cards.
489 *
490 * @return mixed Plugin cards.
491 */
492 public function display_rows() { //phpcs:ignore -- NOSONAR - complex.
493 $search_term = isset( $_GET['s'] ) ? sanitize_text_field( wp_unslash( $_GET['s'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification
494
495 if ( ! empty( $search_term ) && $this->is_mainwp_search( $search_term ) ) {
496 $this->render_mainwp_child_card();
497 $this->items = array_filter(
498 $this->items,
499 function ( $plugin ) {
500 $slug = is_array( $plugin ) ? ( $plugin['slug'] ?? '' ) : ( $plugin->slug ?? '' );
501 return 'mainwp-child' !== $slug;
502 }
503 );
504 }
505
506 $plugins_allowedtags = array(
507 'a' => array(
508 'href' => array(),
509 'title' => array(),
510 'target' => array(),
511 ),
512 'abbr' => array( 'title' => array() ),
513 'acronym' => array( 'title' => array() ),
514 'code' => array(),
515 'pre' => array(),
516 'em' => array(),
517 'strong' => array(),
518 'ul' => array(),
519 'ol' => array(),
520 'li' => array(),
521 'p' => array(),
522 'br' => array(),
523 );
524
525 $plugins_group_titles = array(
526 'Performance' => _x( 'Performance', 'Plugin installer group title', 'mainwp' ),
527 'Social' => _x( 'Social', 'Plugin installer group title', 'mainwp' ),
528 'Tools' => _x( 'Tools', 'Plugin installer group title', 'mainwp' ),
529 );
530
531 $group = null;
532
533 foreach ( (array) $this->items as $plugin ) {
534 if ( is_object( $plugin ) ) {
535 $plugin = (array) $plugin;
536 }
537
538 // Display the group heading if there is one.
539 if ( isset( $plugin['group'] ) && $plugin['group'] != $group ) { //phpcs:ignore -- to valid.
540 if ( isset( $this->groups[ $plugin['group'] ] ) ) {
541 $group_name = $this->groups[ $plugin['group'] ];
542 if ( isset( $plugins_group_titles[ $group_name ] ) ) {
543 $group_name = $plugins_group_titles[ $group_name ];
544 }
545 } else {
546 $group_name = $plugin['group'];
547 }
548
549 // Starting a new group, close off the divs of the last one.
550 if ( ! empty( $group ) ) {
551 echo '</div></div>';
552 }
553
554 echo '<div class="plugin-group"><h3>' . esc_html( $group_name ) . '</h3>';
555 // Needs an extra wrapping div for nth-child selectors to work.
556 echo '<div class="plugin-items">';
557
558 $group = $plugin['group'];
559 }
560 $title = wp_kses( $plugin['name'], $plugins_allowedtags );
561
562 // Remove any HTML from the description.
563 $description = wp_strip_all_tags( $plugin['short_description'] );
564 $version = wp_kses( $plugin['version'], $plugins_allowedtags );
565
566 $author = wp_kses( $plugin['author'], $plugins_allowedtags );
567 if ( ! empty( $author ) ) {
568 /* translators: %s: Plugin author name. */
569 $author = ' <cite>' . sprintf( esc_html__( 'By %s', 'mainwp' ), $author ) . '</cite>';
570 }
571
572 $details_link = self_admin_url( 'plugin-install.php?tab=plugin-information&plugin=' . $plugin['slug'] . '&url=' . ( isset( $plugin['PluginURI'] ) ? rawurlencode( $plugin['PluginURI'] ) : '' ) . '&name=' . rawurlencode( $plugin['name'] ) );
573
574 if ( ! empty( $plugin['icons']['svg'] ) ) {
575 $plugin_icon_url = $plugin['icons']['svg'];
576 } elseif ( ! empty( $plugin['icons']['2x'] ) ) {
577 $plugin_icon_url = $plugin['icons']['2x'];
578 } elseif ( ! empty( $plugin['icons']['1x'] ) ) {
579 $plugin_icon_url = $plugin['icons']['1x'];
580 } else {
581 $plugin_icon_url = $plugin['icons']['default'];
582 }
583
584 $last_updated_timestamp = strtotime( $plugin['last_updated'] );
585 ?>
586
587 <div class="card plugin-card-<?php echo sanitize_html_class( $plugin['slug'] ); ?>" plugin-slug="<?php echo esc_attr( $plugin['slug'] ); ?>" plugin-name="<?php echo esc_attr( $plugin['name'] ); ?>">
588 <?php
589 /**
590 * Action: mainwp_install_plugin_card_top
591 *
592 * Fires at the plugin card at top on the Install Plugins page.
593 *
594 * @since 4.1
595 */
596 do_action( 'mainwp_install_plugin_card_top' );
597 ?>
598 <div class="content">
599 <a class="right floated mini ui image open-plugin-details-modal" href="<?php echo esc_url( $details_link ); ?>"><img src="<?php echo esc_attr( $plugin_icon_url ); ?>" alt="<?php echo esc_attr( $plugin['name'] ); ?>" /></a>
600 <div class="header">
601 <a class="open-plugin-details-modal" href="<?php echo esc_url( $details_link ); ?>"><?php echo $title; // phpcs:ignore WordPress.Security.EscapeOutput ?></a>
602 </div>
603 <div class="meta"><?php echo $author; // phpcs:ignore WordPress.Security.EscapeOutput ?></div>
604 <div class="description">
605 <?php echo esc_html( wp_strip_all_tags( $description ) ); ?>
606 </div>
607 </div>
608 <div class="extra content">
609 <div class="ui stacking grid">
610 <div class="four wide left aligned column">
611 <?php
612 wp_star_rating(
613 array(
614 'rating' => $plugin['rating'],
615 'type' => 'percent',
616 'number' => $plugin['num_ratings'],
617 )
618 );
619 ?>
620 </div>
621 <div class="twelve wide right aligned column"><span class="ui small text"><?php
622 /* translators: %s: Human-readable time difference. */
623 printf( esc_html__( 'Updated %s ago', 'mainwp' ), human_time_diff( $last_updated_timestamp ) ); // phpcs:ignore WordPress.Security.EscapeOutput ?></span></div>
624 </div>
625 </div>
626 <div class="extra content">
627 <div class="ui two column grid">
628 <div class="left aligned middle aligned column">
629 <a href="<?php echo esc_attr( $details_link ); ?>" class="ui mini button open-plugin-details-modal"><?php echo esc_html__( 'Plugin Details', 'mainwp' ); ?></a>
630 </div>
631 <div class="right aligned middle aligned column">
632 <div class="ui radio checkbox">
633 <input name="install-plugin" type="radio" id="install-plugin-<?php echo sanitize_html_class( $plugin['slug'] ); ?>" plugin-name="<?php echo esc_attr( $title ); ?>" plugin-version="<?php echo esc_attr( $version ); ?>">
634 <label><?php esc_html_e( 'Install Plugin', 'mainwp' ); ?></label>
635 </div>
636 </div>
637 </div>
638 </div>
639 <?php
640 /**
641 * Action: mainwp_install_plugin_card_bottom
642 *
643 * Fires at the plugin card at bottom on the Install Plugins page.
644 *
645 * @since 4.1
646 */
647 do_action( 'mainwp_install_plugin_card_bottom', $plugin );
648 ?>
649 </div>
650 <?php
651 }
652 ?>
653 <script type="text/javascript">
654 jQuery( document ).ready( function() {
655 jQuery( '.card .ui.star.rating' ).rating();
656 } );
657 </script>
658 <?php
659 // Close off the group divs of the last one.
660 if ( ! empty( $group ) ) {
661 echo '</div></div>';
662 }
663 }
664 }
665