PluginProbe
Plugin Report / 1.6
Plugin Report v1.6
2.2.4 trunk 1.1 1.2 1.3 1.4 1.5 1.6 1.6.1 1.7 1.8 1.8.1 1.8.2 1.8.3 1.9 1.9.1 1.9.2 1.9.3 2.0.0 2.0.1 2.0.2 2.1 2.1.1 2.2.0 2.2.1 All 27 releases
plugin-report / rt-plugin-report.php

rt-plugin-report.php in Plugin Report 1.6, at rt-plugin-report.php

608 lines 21.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Plugin Name: Plugin Report
4 * Plugin URI: https://roytanck.com/?p=277
5 * Description: Provides detailed information about currently installed plugins
6 * Version: 1.6
7 * Requires at least: 4.6
8 * Requires PHP: 5.6
9 * Author: Roy Tanck
10 * Author URI: https://roytanck.com
11 * License: GPLv3
12 */
13
14 // If called without WordPress, exit.
15 if ( ! defined( 'ABSPATH' ) ) {
16 exit;
17 }
18
19
20 if ( is_admin() && ! class_exists( 'RT_Plugin_Report' ) ) {
21
22 class RT_Plugin_Report {
23
24 // CSS class constants.
25 const CSS_CLASS_LOW = 'pr-risk-low';
26 const CSS_CLASS_MED = 'pr-risk-medium';
27 const CSS_CLASS_HIGH = 'pr-risk-high';
28
29 // Other class constants.
30 const PLUGIN_VERSION = '1.5';
31 const COLS_PER_ROW = 7;
32 const CACHE_LIFETIME = DAY_IN_SECONDS;
33 const CACHE_LIFETIME_NOREPO = WEEK_IN_SECONDS;
34
35 /**
36 * Constructor
37 */
38 public function __construct() {
39 // Intentionally left blank.
40 }
41
42
43 /**
44 * Set up things like hooks and such
45 */
46 public function init() {
47 // Hook for the admin page.
48 if ( is_multisite() ) {
49 add_action( 'network_admin_menu', array( $this, 'register_settings_page' ) );
50 } else {
51 add_action( 'admin_menu', array( $this, 'register_settings_page' ) );
52 }
53 // Hook for the admin js.
54 add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) );
55 // Add the AJAX hook.
56 add_action( 'wp_ajax_rt_get_plugin_info', array( $this, 'get_plugin_info' ) );
57 // Hook into the WP Upgrader to selectively delete cache items.
58 add_action( 'upgrader_process_complete', array( $this, 'upgrade_delete_cache_items' ), 10 ,2 );
59 }
60
61
62 /**
63 * Add a new options page to the network admin
64 */
65 public function register_settings_page() {
66 add_plugins_page(
67 esc_html_x( 'Plugin Report', 'Page and menu title', 'plugin-report' ),
68 esc_html_x( 'Plugin Report', 'Page and menu title', 'plugin-report' ),
69 'manage_options',
70 'plugin_report',
71 array( $this, 'settings_page' )
72 );
73 }
74
75
76 /**
77 * Render the options page
78 */
79 public function settings_page() {
80 // Check user capabilities, just to be sure.
81 if ( ! current_user_can( 'manage_options' ) ) {
82 wp_die();
83 }
84 // Assemble information we'll need.
85 global $wp_version;
86 $plugins = get_plugins();
87
88 // Check wether a core update is available.
89 $wp_latest = $this->check_core_updates();
90
91 // Refresh the cache, but only if this is a fresh timestamp (not if the page has been refreshed with the timestamp still in the URL).
92 if ( isset( $_GET['clear_cache'] ) ) {
93 $new_timestamp = intval( $_GET['clear_cache'] );
94 $last_timestamp = intval( get_site_transient( 'plugin_report_cache_cleared' ) );
95 if ( ! $last_timestamp || $new_timestamp > $last_timestamp ) {
96 $this->clear_cache();
97 set_site_transient( 'plugin_report_cache_cleared', $new_timestamp, self::CACHE_LIFETIME );
98 }
99 }
100
101 // Start the page's output.
102 echo '<div class="wrap">';
103 echo '<h1>' . esc_html_x( 'Plugin Report', 'Page and menu title', 'plugin-report' ) . '</h1>';
104 echo '<p>';
105 $version_temp = '<span class="' . $this->get_version_risk_classname( $wp_version, $wp_latest ) . '">' . $wp_version . '</span>';
106 /* translators: %1$s: Current WordPress version number, %2$s: Current PHP version number */
107 echo sprintf( __( 'Currently running WordPress version %1$s and PHP version %2$s.', 'plugin-report' ), $version_temp, phpversion() );
108 if ( version_compare( $wp_version, $wp_latest, '<' ) ) {
109 /* translators: %s = Available new version number */
110 echo sprintf( ' (' . esc_html__( 'An upgrade to %s is available', 'plugin-report' ) . ')', $wp_latest );
111 }
112 echo '</p>';
113 echo '<p>';
114 // Clear cache and reload.
115 if ( is_multisite() ) {
116 $page_url = 'network/plugins.php?page=plugin_report';
117 } else {
118 $page_url = 'plugins.php?page=plugin_report';
119 }
120 echo '<a href="' . admin_url( $page_url . '&clear_cache=' . current_time( 'timestamp' ) ) . '">' . esc_html__( 'Clear cached plugin data and reload', 'plugin-report' ) . '</a>';
121 echo '</p>';
122 echo '<h3>' . esc_html__( 'Currently installed plugins', 'plugin-report' ) . '</h3>';
123 echo '<p id="plugin-report-progress"></p>';
124 echo '<p>';
125
126 // The report's main table.
127 echo '<table id="plugin-report-table" class="wp-list-table widefat fixed striped">';
128 echo '<thead>';
129 echo '<tr>';
130 echo '<th>' . esc_html__( 'Name', 'plugin-report' ) . '</th>';
131 echo '<th>' . esc_html__( 'Author', 'plugin-report' ) . '</th>';
132 echo '<th>' . esc_html__( 'Activated', 'plugin-report' ) . '</th>';
133 echo '<th>' . esc_html__( 'Installed version', 'plugin-report' ) . '</th>';
134 echo '<th>' . esc_html__( 'Last update', 'plugin-report' ) . '</th>';
135 echo '<th>' . esc_html__( 'Tested up to WP version', 'plugin-report' ) . '</th>';
136 echo '<th>' . esc_html__( 'Rating', 'plugin-report' ) . '</th>';
137 echo '</tr>';
138 echo '</thead>';
139 echo '<tbody>';
140
141 foreach ( $plugins as $key => $plugin ) {
142 $slug = $this->get_plugin_slug( $key );
143 $cache_key = $this->create_cache_key( $slug );
144 $cache = get_site_transient( $cache_key );
145 if ( $cache ) {
146 // Use the cached report to create a table row.
147 echo $this->render_table_row( $cache );
148 } else {
149 // Render a special table row that's used as a signal to the front-end js that new data is needed.
150 echo '<tr class="plugin-report-row-temp-' . $slug . '"><td colspan="' . self::COLS_PER_ROW . '">' . esc_html__( 'Loading...', 'plugin-report' ) . '</td></tr>';
151 }
152 }
153
154 echo '</tbody>';
155 echo '</table>';
156 echo '</p>';
157
158 echo '<p id="plugin-report-buttons"></p>';
159
160 // Wrap up.
161 echo '</div>';
162 }
163
164
165 /**
166 * Enqueue admin javascript
167 */
168 public function enqueue_assets( $hook ) {
169 // Check if we're on the right screen.
170 if ( 'plugins_page_plugin_report' != $hook ) {
171 return;
172 }
173 // register the plugin's admin js, and require jquery
174 wp_enqueue_script( 'plugin-report-js', plugins_url( '/js/plugin-report.js', __FILE__ ), array( 'jquery' ), self::PLUGIN_VERSION );
175 // add some variables to the page, to be used by the javascript
176 $slugs = $this->get_plugin_slugs();
177 $slugs_str = implode( ',', $slugs );
178 $vars = array(
179 'plugin_slugs' => $slugs_str,
180 'ajax_nonce' => wp_create_nonce( 'plugin_report_nonce' ),
181 'export_btn' => __( 'Export .xls file', 'plugin-report' ),
182 );
183 wp_localize_script( 'plugin-report-js', 'plugin_report_vars', $vars );
184 // Enqueue admin CSS file.
185 wp_enqueue_style( 'plugin-report-css', plugin_dir_url( __FILE__ ) . 'css/plugin-report.css', array(), self::PLUGIN_VERSION );
186 }
187
188
189 /**
190 * Get the slugs for all currently installed plugins
191 */
192 private function get_plugin_slugs() {
193 $plugins = get_plugins();
194 $slugs = array();
195 foreach ( $plugins as $key => $plugin ) {
196 $slugs[] = $this->get_plugin_slug( $key );
197 }
198 return $slugs;
199 }
200
201
202 /**
203 * Convert a plugin's file path into its slug
204 */
205 private function get_plugin_slug( $file ) {
206 if ( strpos( $file, '/' ) !== false ) {
207 $parts = explode( '/', $file );
208 } else {
209 $parts = explode( '.', $file );
210 }
211 return sanitize_title( $parts[0] );
212 }
213
214
215 /**
216 * AJAX handler
217 * Returns a full html table row with the plugin's data
218 */
219 public function get_plugin_info() {
220
221 // Check the ajax nonce, display an error if the check fails.
222 if ( ! check_ajax_referer( 'plugin_report_nonce', 'nonce', false ) ) {
223 wp_die();
224 }
225
226 // Check user capabilites, just to be sure.
227 if ( ! current_user_can( 'manage_options' ) ) {
228 wp_die();
229 }
230
231 // Check if get_plugins() function exists.
232 if ( ! function_exists( 'plugins_api' ) ) {
233 require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
234 }
235
236 $slug = sanitize_title( $_POST['slug'] );
237
238 $report = $this->assemble_plugin_report( $slug );
239
240 if ( $report ) {
241 $table_row = $this->render_table_row( $report );
242 } else {
243 $table_row = $this->render_error_row( esc_html__( 'No plugin data available.', 'plugin-report' ) );
244 }
245
246 // Formulate a response.
247 $response = array(
248 'html' => $table_row,
249 'message' => 'Success!',
250 );
251 // Return the response.
252 echo json_encode( $response );
253
254 wp_die();
255 }
256
257
258 /**
259 * Gather all the info we can get about a plugin.
260 * Uses transient caching to avoid doing repo API calls on every page visit
261 */
262 private function assemble_plugin_report( $slug ) {
263 if ( ! empty( $slug ) ) {
264 $report = array();
265 $cache_key = $this->create_cache_key( $slug );
266 $cache = get_site_transient( $cache_key );
267 $plugins = get_plugins();
268
269 if ( empty( $cache ) ) {
270
271 // Add the plugin's slug to the report.
272 $report['slug'] = $slug;
273
274 // Get the locally available info, and add it to the report.
275 foreach ( $plugins as $key => $plugin ) {
276 if ( $this->get_plugin_slug( $key ) == $slug ) {
277 $report['local_info'] = $plugin;
278 $report['file_path'] = $key;
279 break;
280 }
281 }
282
283 // Use the wordpress.org repository API to get detailed information.
284 $args = array(
285 'slug' => $slug,
286 'fields' => array(
287 'description' => false,
288 'sections' => false,
289 'tags' => false,
290 'version' => true,
291 'tested' => true,
292 'requires' => true,
293 'requires_php' => true,
294 'compatibility' => true,
295 'author' => true,
296 ),
297 );
298 $returned_object = plugins_api( 'plugin_information', $args );
299
300 // Add the repo info to the report.
301 if ( ! is_wp_error( $returned_object ) ) {
302 $report['repo_info'] = maybe_unserialize( $returned_object );
303 // Cache the report.
304 set_site_transient( $cache_key, $report, self::CACHE_LIFETIME );
305 } else {
306 // Store the error code and message in the report.
307 $report['repo_error_code'] = $returned_object->get_error_code();
308 $report['repo_error_message'] = $returned_object->get_error_message();
309 // Cache for an extra long time when the plugin is not in the repo.
310 set_site_transient( $cache_key, $report, self::CACHE_LIFETIME_NOREPO );
311 }
312 } else {
313 $report = $cache;
314 }
315
316 return $report;
317
318 } else {
319 return null;
320 }
321
322 }
323
324
325 /**
326 * From a report, generate an HTML table row with relevant data for the plugin
327 */
328 private function render_table_row( $report ) {
329 // Get the latest WP release version number.
330 $wp_latest = $this->check_core_updates();
331 // Check if the report is valid.
332 if ( $report == null ) {
333 $html = $this->render_error_row( esc_html__( 'No plugin data available.', 'plugin-report' ) );
334 } else {
335 // Start the new table row.
336 $html = '<tr class="plugin-report-row-' . $report['slug'] . '">';
337 // Name.
338 if( isset( $report['local_info']['PluginURI'] ) && !empty( $report['local_info']['PluginURI'] ) ){
339 $html .= '<td><a href="' . $report['local_info']['PluginURI'] . '"><strong>' . $report['local_info']['Name'] . '</strong></a></td>';
340 } else {
341 $html .= '<td><strong>' . $report['local_info']['Name'] . '</strong></td>';
342 }
343 // Author.
344 if( isset( $report['local_info']['AuthorURI'] ) && !empty( $report['local_info']['AuthorURI'] ) ){
345 $html .= '<td><a href="' . $report['local_info']['AuthorURI'] . '">' . $report['local_info']['Author'] . '</a></td>';
346 } else {
347 $html .= '<td>' . $report['local_info']['Author'] . '</td>';
348 }
349 // Activated.
350 $active = __( 'Please clear cache to update', 'plugin-report' );
351 $css_class = self::CSS_CLASS_MED;
352 if( is_multisite() ){
353 $activation_status = $this->get_multisite_activation( $report['file_path'] );
354 if( $activation_status['network'] === true ){
355 $css_class = self::CSS_CLASS_LOW;
356 $html .= '<td class="' . $css_class . '">' . __( 'Network activated', 'plugin-report' ) . '</td>';
357 } else {
358 $css_class = ( $activation_status['active'] > 0 ) ? self::CSS_CLASS_LOW : self::CSS_CLASS_HIGH;
359 $html .= '<td class="' . $css_class . '">' . $activation_status['active'] . '/' . $activation_status['sites'] . '</td>';
360 }
361 } else {
362 if( isset( $report['file_path'] ) ){
363 $active = is_plugin_active( $report['file_path'] ) ? __( 'Yes', 'plugin-report' ) : __( 'No', 'plugin-report' );
364 $css_class = is_plugin_active( $report['file_path'] ) ? self::CSS_CLASS_LOW : self::CSS_CLASS_HIGH;
365 }
366 $html .= '<td class="' . $css_class . '">' . $active . '</td>';
367 }
368 // Installed / available version.
369 if( isset( $report['repo_info'] ) ){
370 $css_class = $this->get_version_risk_classname( $report['local_info']['Version'], $report['repo_info']->version );
371 $html .= '<td class="' . $css_class . '">';
372 $html .= $report['local_info']['Version'];
373 if ( $report['local_info']['Version'] != $report['repo_info']->version ) {
374 // Any platform upgrades needed?
375 global $wp_version;
376 $needs_php_upgrade = isset( $report['repo_info']->requires_php) ? version_compare( phpversion(), $report['repo_info']->requires_php, '<' ) : false;
377 $needs_wp_upgrade = isset( $report['repo_info']->requires ) ? version_compare( $wp_version, $report['repo_info']->requires, '<' ) : false;
378 // Create the additional message.
379 if ( $needs_wp_upgrade && $needs_php_upgrade ) {
380 /* translators: %1$s: Plugin version number, %2$s: WP version number, %3$s: PHP version number */
381 $html .= ' <span class="pr-additional-info">' . sprintf( esc_html__( '(%1$s available, requires WP %2$s and PHP %3$s)', 'plugin-report'), $report['repo_info']->version, $report['repo_info']->requires, $report['repo_info']->requires_php ) . '</span>';
382 } elseif ( $needs_wp_upgrade ) {
383 /* translators: %1$s: Plugin version number, %2$s: WP version number. */
384 $html .= ' <span class="pr-additional-info">' . sprintf( esc_html__( '(%1$s available, requires WP %2$s)', 'plugin-report'), $report['repo_info']->version, $report['repo_info']->requires ) . '</span>';
385 } elseif ( $needs_php_upgrade ) {
386 /* translators: %1$s: Plugin version number, %2$s: PHP version number. */
387 $html .= ' <span class="pr-additional-info">' . sprintf( esc_html__( '(%1$s available, requires PHP %2$s)', 'plugin-report'), $report['repo_info']->version, $report['repo_info']->requires_php ) . '</span>';
388 } else {
389 /* translators: %s: Plugin version number. */
390 $html .= ' <span class="pr-additional-info">' . sprintf( esc_html__( '(%s available)', 'plugin-report'), $report['repo_info']->version ) . '</span>';
391 }
392 }
393 $html .= '</td>';
394 } else {
395 $html .= '<td>' . $report['local_info']['Version'] . '</td>';
396 }
397 // Last updates.
398 if( isset( $report['repo_info'] ) ){
399 $time_update = new DateTime( $report['repo_info']->last_updated );
400 $time_diff = human_time_diff( $time_update->getTimestamp(), current_time( 'timestamp' ) );
401 $css_class = $this->get_timediff_risk_classname( current_time( 'timestamp' ) - $time_update->getTimestamp() );
402 $html .= '<td class="' . $css_class . '">' . $time_diff . '</td>';
403 } else {
404 $html .= $this->render_error_cell();
405 }
406 // Tested up to.
407 if( isset( $report['repo_info'] ) ){
408 $css_class = $this->get_version_risk_classname( $report['repo_info']->tested, $wp_latest );
409 $html .= '<td class="' . $css_class . '">' . $report['repo_info']->tested . '</td>';
410 } else {
411 $html .= $this->render_error_cell();
412 }
413 // Overall user rating.
414 if( isset( $report['repo_info'] ) ){
415 $css_class = ( intval( $report['repo_info']->num_ratings ) > 0 ) ? $this->get_percentage_risk_classname( intval( $report['repo_info']->rating ) ) : '';
416 $value_text = ( ( intval( $report['repo_info']->num_ratings ) > 0 ) ? $report['repo_info']->rating . '%' : esc_html__( 'No data available', 'plugin-report' ) );
417 $html .= '<td class="' . $css_class . '">' . $value_text . '</td>';
418 } else {
419 $html .= $this->render_error_cell();
420 }
421 // Close the new table row.
422 $html .= '</tr>';
423 }
424 return $html;
425 }
426
427
428 /**
429 * Format an error message as a table row, so we can return it to javascript
430 */
431 private function render_error_row( $message ) {
432 return '<tr class="pluginreport-row-error"><td colspan="' . self::COLS_PER_ROW . '">' . $message . '</td></tr>';
433 }
434
435
436 /**
437 * Format an error message as a table cell, so we can return it to javascript
438 */
439 private function render_error_cell( $message = null ) {
440 if( !$message ){
441 $message = esc_html__( 'No data available', 'plugin-report' );
442 }
443 return '<td class="pluginreport-cell-error">' . $message . '</td>';
444 }
445
446
447 /**
448 * Figure out what CSS class to use based on current and optimal version numbers
449 */
450 private function get_version_risk_classname( $available, $optimal ) {
451 // If the version match, indicate low risk.
452 if ( version_compare( $available, $optimal, '==' ) ) {
453 return self::CSS_CLASS_LOW;
454 }
455 // Else, indicate high risk.
456 return self::CSS_CLASS_HIGH;
457 }
458
459
460 /**
461 * Assess the risk associated with low ratings or poor compatibility feedback, return corresponding CSS class
462 */
463 private function get_percentage_risk_classname( $perc ) {
464 if ( $perc < 70 ) {
465 return self::CSS_CLASS_HIGH;
466 }
467 if ( $perc < 90 ) {
468 return self::CSS_CLASS_MED;
469 }
470 return self::CSS_CLASS_LOW;
471 }
472
473
474 /**
475 * Assess the risk associated with low ratings or poor compatibility feedback, return corresponding CSS class
476 */
477 private function get_timediff_risk_classname( $time_diff ) {
478 $days = $time_diff / ( DAY_IN_SECONDS );
479 if ( $days > 365 ) {
480 return self::CSS_CLASS_HIGH;
481 }
482 if ( $days > 90 ) {
483 return self::CSS_CLASS_MED;
484 }
485 return self::CSS_CLASS_LOW;
486 }
487
488
489 /**
490 * Get the latest available WordPress version using WP core functions
491 * This way, we don't need to do any API calls. WP check this periodically anyway.
492 */
493 private function check_core_updates() {
494 global $wp_version;
495 $update = get_preferred_from_update_core();
496 // Bail out of no valid response, or false.
497 if ( ! $update || $update == false ) {
498 return $wp_version;
499 }
500 // If latest, return current version number.
501 if ( $update->response == 'latest' ) {
502 return $wp_version;
503 }
504 // Return the preferred update's version number.
505 return $update->version;
506 }
507
508
509 /**
510 * Gather statistics about a plugin's activation on a multisite install
511 */
512 private function get_multisite_activation( $path ){
513 // Create an array to contain the return values.
514 $activation_status = array(
515 'network' => false,
516 'active' => 0,
517 'sites' => 1,
518 );
519 // Check if the plugin is network activated.
520 $network_plugins = get_site_option( 'active_sitewide_plugins', null );
521 if( array_key_exists( $path, $network_plugins ) ){
522 $activation_status['network'] = true;
523 } else {
524 // Get a list of all sites in the multisite install.
525 $args = array(
526 'number' => 9999,
527 'fields' => 'ids',
528 );
529 $sites = get_sites( $args );
530 // Add the total number of sites to the return array.
531 $activation_status['sites'] = count( $sites );
532 // Loop through the sites to find where the plugin is active.
533 foreach( $sites as $site_id ){
534 $plugins = get_blog_option( $site_id, 'active_plugins', null );
535 if( $plugins ){
536 foreach( $plugins as $plugin_path ){
537 if( $plugin_path === $path ){
538 $activation_status['active']++;
539 }
540 }
541 }
542 }
543 }
544 // Return the data we gathered.
545 return $activation_status;
546 }
547
548
549 /**
550 * Create a cache key that is unique to the provided plugin slug.
551 */
552 private function create_cache_key( $slug ) {
553 // Create a hash for the plugiin slug.
554 $slug_hash = hash( 'sha256', $slug );
555 // Prefix and limit the string to 40 characters to avoid issues with long keys.
556 $cache_key = 'rtpr_' . substr( $slug_hash, 0, 35 );
557 // Return the key.
558 return $cache_key;
559 }
560
561
562 /**
563 * Clear all cached plugin info
564 */
565 private function clear_cache() {
566 // Request a list of all plugins.
567 $plugins = get_plugins();
568 // Loop through the plugins array, and delete cache items.
569 foreach ( $plugins as $key => $plugin ) {
570 $slug = $this->get_plugin_slug( $key );
571 $this->clear_cache_item( $slug );
572 }
573 }
574
575
576 /**
577 * Remove the cache item for a single plugin
578 */
579 private function clear_cache_item( $slug ) {
580 if ( isset( $slug ) ) {
581 $cache_key = $this->create_cache_key( $slug );
582 delete_site_transient( $cache_key );
583 }
584 }
585
586
587 /**
588 * Selectively delete cache for plugins that have been updated.
589 */
590 public function upgrade_delete_cache_items( $upgrader, $data ) {
591 // Check if plugins have been upgraded by WP.
592 if ( isset( $data ) && isset( $data['plugins'] ) && is_array( $data['plugins'] ) ) {
593 // Loop through the plugins, and delete the associated cache items.
594 foreach ( $data['plugins'] as $key => $value ) {
595 $slug = $this->get_plugin_slug( $value );
596 $this->clear_cache_item( $slug );
597 }
598 }
599 }
600
601 }
602
603 // Instantiate the class.
604 $plugin_report_instance = new RT_Plugin_Report();
605 $plugin_report_instance->init();
606
607 }
608