PluginProbe
Plugin Report / 1.5
Plugin Report v1.5
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.5, at rt-plugin-report.php

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