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

476 lines 15.7 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
5 * Description: Provides detailed information about currently installed plugins
6 * Version: 1.1
7 * Requires at least: 4.6
8 * Requires PHP: 5.6
9 * Author: Roy Tanck
10 * Text Domain: plugin-report
11 * Domain Path: /languages
12 * License: GPLv3
13 */
14
15 // If called without WordPress, exit.
16 if ( ! defined( 'ABSPATH' ) ) {
17 exit;
18 }
19
20
21 if ( is_admin() && ! class_exists( 'RT_Plugin_Report' ) ) {
22
23 class RT_Plugin_Report {
24
25 public $cols_per_row = 7;
26 public $cache_lifetime = DAY_IN_SECONDS;
27 public $cache_lifetime_norepo = WEEK_IN_SECONDS;
28
29 /**
30 * Constructor
31 */
32 function __construct() {
33 // Intentionally left blank.
34 }
35
36
37 /**
38 * Set up things like hooks and such
39 */
40 public function init() {
41 // Hook for the admin page.
42 if ( is_multisite() ) {
43 add_action( 'network_admin_menu', array( $this, 'register_settings_page' ) );
44 } else {
45 add_action( 'admin_menu', array( $this, 'register_settings_page' ) );
46 }
47 // Hook for the admin js.
48 add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_js' ) );
49 // Add the AJAX hook.
50 add_action( 'wp_ajax_rt_get_plugin_info', array( $this, 'get_plugin_info' ) );
51 }
52
53
54 /**
55 * Add a new options page to the network admin
56 */
57 public function register_settings_page() {
58 add_plugins_page(
59 esc_html__( 'Plugin Report', 'plugin-report' ),
60 esc_html__( 'Plugin Report', 'plugin-report' ),
61 'manage_options',
62 'rt_plugin_report',
63 array( $this, 'settings_page' )
64 );
65 }
66
67
68 /**
69 * Render the options page
70 */
71 public function settings_page() {
72 // Check user capabilites, just to be sure.
73 if ( ! current_user_can( 'manage_options' ) ) {
74 die();
75 }
76 // Assemble information we'll need.
77 global $wp_version;
78 $plugins = get_plugins();
79
80 // Check wether a core update is available.
81 $wp_latest = $this->check_core_updates();
82
83 // 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).
84 if ( isset( $_GET['clear_cache'] ) ) {
85 $new_timestamp = intval( $_GET['clear_cache'] );
86 $last_timestamp = intval( get_site_transient( 'rt_plugin_report_cache_cleared' ) );
87 if ( ! $last_timestamp || $new_timestamp > $last_timestamp ) {
88 $this->clear_cache();
89 set_site_transient( 'rt_plugin_report_cache_cleared', $new_timestamp, $this->cache_lifetime );
90 }
91 }
92
93 // Start the page's output.
94 echo '<div class="wrap">';
95 echo '<h1>' . esc_html__( 'Plugin Report', 'plugin-report' ) . '</h1>';
96 echo '<p>';
97 $version_temp = '<span class="' . $this->get_version_risk_classname( $wp_version, $wp_latest ) . '">' . $wp_version . '</span>';
98 /* translators: %s = Current WordPress version number */
99 echo sprintf( __( 'Currently running WordPress version: %s.', 'plugin-report' ), $version_temp );
100 if ( version_compare( $wp_version, $wp_latest, '<' ) ) {
101 /* translators: %s = Available new version number */
102 echo sprintf( ' (' . esc_html__( 'An upgrade to %s is available', 'plugin-report' ) . ')', $wp_latest );
103 }
104 echo '</p>';
105 echo '<p>';
106 // Clear cache and reload.
107 if ( is_multisite() ) {
108 $page_url = 'network/plugins.php?page=rt_plugin_report';
109 } else {
110 $page_url = 'plugins.php?page=rt_plugin_report';
111 }
112 echo '<a href="' . admin_url( $page_url . '&clear_cache=' . current_time( 'timestamp' ) ) . '">' . esc_html__( 'Clear cached plugin data and reload', 'plugin-report' ) . '</a>';
113 echo '</p>';
114 echo '<h3>' . esc_html__( 'Currently installed plugins', 'plugin-report' ) . '</h3>';
115 echo '<p id="rt-plugin-report-progress"></p>';
116 echo '<p>';
117
118 // The report's main table.
119 echo '<table id="rt-plugin-report-table" class="wp-list-table widefat fixed striped">';
120 echo '<tr>';
121 echo '<th>' . esc_html__( 'Name', 'plugin-report' ) . '</th>';
122 echo '<th>' . esc_html__( 'Author', 'plugin-report' ) . '</th>';
123 echo '<th>' . esc_html__( 'Installed version', 'plugin-report' ) . '</th>';
124 echo '<th>' . esc_html__( 'Last update', 'plugin-report' ) . '</th>';
125 echo '<th>' . esc_html__( 'Tested', 'plugin-report' ) . '</th>';
126 echo '<th>' . esc_html__( 'Compatibility reports', 'plugin-report' ) . '</th>';
127 echo '<th>' . esc_html__( 'Rating', 'plugin-report' ) . '</th>';
128 echo '</tr>';
129
130 foreach ( $plugins as $key => $plugin ) {
131 $slug = $this->get_plugin_slug( $key );
132 $cache_key = 'rt_plugin_report_cache_' . $slug;
133 $cache = get_site_transient( $cache_key );
134 if ( $cache ) {
135 // Use the cached report to create a table row.
136 echo $this->render_table_row( $cache );
137 } else {
138 // Render a special table row that's used as a signal to the front-end js that new data is needed.
139 echo '<tr class="rt-plugin-report-row-temp-' . $slug . '"><td colspan="' . $this->cols_per_row . '">loading...</td></tr>';
140 }
141 }
142
143 echo '</table>';
144
145 echo '<div id="rt-debug"></div>';
146
147 // Wrap up.
148 echo '</p>';
149 echo '</div>';
150 }
151
152
153 /**
154 * Enqueue admin javascript
155 */
156 public function enqueue_js( $hook ) {
157 // Check if we're on the right screen.
158 if ( 'plugins_page_rt_plugin_report' != $hook ) {
159 return;
160 }
161 // register the plugin's admin js, and require jquery
162 wp_enqueue_script( 'rt-plugin-report-js', plugins_url( '/js/rt-plugin-report.js', __FILE__ ), array( 'jquery' ) );
163 // add some variables to the page, to be used by the javascript
164 $slugs = $this->get_plugin_slugs();
165 $slugs_str = implode( ',', $slugs );
166 $vars = array(
167 'plugin_slugs' => $slugs_str,
168 'ajax_nonce' => wp_create_nonce( 'rt_plugin_report_nonce' ),
169 );
170 wp_localize_script( 'rt-plugin-report-js', 'rt_plugin_report_vars', $vars );
171 // Enqueue admin CSS file.
172 wp_enqueue_style( 'rt-plugin-report-css', plugin_dir_url( __FILE__ ) . 'css/rt-plugin-report.css' );
173 }
174
175
176 /**
177 * Get the slugs for all currently installed plugins
178 */
179 public function get_plugin_slugs() {
180 $plugins = get_plugins();
181 $slugs = array();
182 foreach ( $plugins as $key => $plugin ) {
183 $slugs[] = $this->get_plugin_slug( $key );
184 }
185 return $slugs;
186 }
187
188
189 /**
190 * Convert a plugin's file path into its slug
191 */
192 public function get_plugin_slug( $file ) {
193 if ( strpos( $file, '/' ) !== false ) {
194 $parts = explode( '/', $file );
195 } else {
196 $parts = explode( '.', $file );
197 }
198 return sanitize_title( $parts[0] );
199 }
200
201
202 /**
203 * AJAX handler
204 * Returns a full html table row with the plugin's data
205 */
206 public function get_plugin_info() {
207
208 // Check the ajax nonce, display an error if the check fails.
209 if ( ! check_ajax_referer( 'rt_plugin_report_nonce', 'nonce', false ) ) {
210 echo 'oops';
211 die();
212 }
213
214 // Check user capabilites, just to be sure.
215 if ( ! current_user_can( 'manage_options' ) ) {
216 die();
217 }
218
219 // Check if get_plugins() function exists.
220 if ( ! function_exists( 'plugins_api' ) ) {
221 require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
222 }
223
224 $slug = sanitize_title( $_POST['slug'] );
225
226 $report = $this->assemble_plugin_report( $slug );
227
228 if ( $report ) {
229 $table_row = $this->render_table_row( $report );
230 } else {
231 $table_row = $this->render_error_row( esc_html__( 'No plugin data available.', 'plugin-report' ) );
232 }
233
234 // Formulate a response.
235 $response = array(
236 'html' => $table_row,
237 'message' => 'Success!',
238 );
239 // Return the response.
240 echo json_encode( $response );
241
242 wp_die();
243 }
244
245
246 /**
247 * Gather all the info we can get about a plugin.
248 * Uses transient caching to avoid doing repo API calls on every page visit
249 */
250 public function assemble_plugin_report( $slug ) {
251 if ( ! empty( $slug ) ) {
252 $report = array();
253 $cache_key = 'rt_plugin_report_cache_' . $slug;
254 $cache = get_site_transient( $cache_key );
255 $plugins = get_plugins();
256
257 if ( empty( $cache ) ) {
258
259 // Add the plugin's slug to the report.
260 $report['slug'] = $slug;
261
262 // Get the locally available info, and add it to the report.
263 foreach ( $plugins as $key => $plugin ) {
264 if ( $this->get_plugin_slug( $key ) == $slug ) {
265 $report['local_info'] = $plugin;
266 break;
267 }
268 }
269
270 // Use the wordpress.org repository API to get detailed information.
271 $args = array(
272 'slug' => $slug,
273 'fields' => array(
274 'description' => false,
275 'sections' => false,
276 'tags' => false,
277 'version' => true,
278 'tested' => true,
279 'requires' => true,
280 'compatibility' => true,
281 'author' => true,
282 ),
283 );
284 $returned_object = plugins_api( 'plugin_information', $args );
285
286 // Add the repo info to the report.
287 if ( ! is_wp_error( $returned_object ) ) {
288 $report['repo_info'] = maybe_unserialize( $returned_object );
289 // Cache the report.
290 set_site_transient( $cache_key, $report, $this->cache_lifetime );
291 } else {
292 // Cache for an extra long time when the plgin is not in the repo.
293 set_site_transient( $cache_key, $report, $this->cache_lifetime_norepo );
294 }
295 } else {
296 $report = $cache;
297 }
298
299 return $report;
300
301 } else {
302 return null;
303 }
304
305 }
306
307
308 /**
309 * From a report, generate an HTML table row with relevant data for the plugin
310 */
311 public function render_table_row( $report ) {
312 // Get the latest WP release version number.
313 $wp_latest = $this->check_core_updates();
314 // Check if the report is valid.
315 if ( $report == null ) {
316 $html = $this->render_error_row( esc_html__( 'No plugin data available.', 'plugin-report' ) );
317 } elseif ( ! isset( $report['repo_info'] ) ) {
318 /* translators: %s = Slug of the plugin */
319 $html = $this->render_error_row( sprintf( esc_html__( 'This plugin "%s" does not appear to be in the wordpress.org repository.', 'plugin-report' ), $report['slug'] ) );
320 } else {
321 $html = '<tr class="rt-plugin-report-row-' . $report['slug'] . '">';
322 // Name.
323 $html .= '<td><a href="https://wordpress.org/plugins/' . $report['slug'] . '">' . $report['repo_info']->name . '</a></td>';
324 // Author.
325 $html .= '<td>' . $report['repo_info']->author . '</td>';
326 // Installed / available version.
327 $html .= '<td><span class="' . $this->get_version_risk_classname( $report['local_info']['Version'], $report['repo_info']->version ) . '">';
328 $html .= $report['local_info']['Version'] . '</span>';
329 if ( $report['local_info']['Version'] != $report['repo_info']->version ) {
330 $html .= ' (' . $report['repo_info']->version . ' available)';
331 }
332 $html .= '</td>';
333 // Last updates.
334 $time_update = new DateTime( $report['repo_info']->last_updated );
335 $time_diff = human_time_diff( $time_update->getTimestamp(), current_time( 'timestamp' ) );
336 $css_class = $this->get_timediff_risk_classname( current_time( 'timestamp' ) - $time_update->getTimestamp() );
337 $html .= '<td class="' . $css_class . '">' . $time_diff . '</td>';
338 // Tested up to.
339 $html .= '<td class="' . $this->get_version_risk_classname( $report['repo_info']->tested, $wp_latest ) . '">' . $report['repo_info']->tested . '</td>';
340 // User-reported compatibility info.
341 $html .= '<td>' . $this->format_compatibility( $report['repo_info']->compatibility ) . '</td>';
342 // Overall user rating.
343 $css_class = ( intval( $report['repo_info']->num_ratings ) > 0 ) ? $this->get_percentage_risk_classname( intval( $report['repo_info']->rating ) ) : '';
344 $html .= '<td class="' . $css_class . '">' . ( ( intval( $report['repo_info']->num_ratings ) > 0 ) ? $report['repo_info']->rating . '%' : esc_html__( 'No data available', 'plugin-report' ) ) . '</td>';
345 $html .= '</tr>';
346 }
347 return $html;
348 }
349
350
351 /**
352 * Format an error message as a table row, so we can return it to javascript
353 */
354 public function render_error_row( $message ) {
355 return '<tr class="rt-pluginreport-row-error"><td colspan="' . $this->cols_per_row . '">' . $message . '</td></tr>';
356 }
357
358
359 /**
360 * Figure out what CSS class to use based on current and optimal version numbers
361 */
362 public function get_version_risk_classname( $available, $optimal ) {
363 // If the version match, indicate low risk.
364 if ( version_compare( $available, $this->major_release_version_nr( $optimal ), '<=' ) == 0 ) {
365 return 'rt-risk-low';
366 }
367 // If major version match, indicate medium risk.
368 if ( version_compare( $this->major_release_version_nr( $available ), $this->major_release_version_nr( $optimal ) ) == 0 ) {
369 return 'rt-risk-medium';
370 }
371 // Else, indicate high risk.
372 return 'rt-risk-high';
373 }
374
375
376 /**
377 * Assess the risk associated with low ratings or poor compatibility feedback, return corresponding CSS class
378 */
379 public function get_percentage_risk_classname( $perc ) {
380 if ( $perc < 70 ) {
381 return 'rt-risk-high';
382 }
383 if ( $perc < 90 ) {
384 return 'rt-risk-medium';
385 }
386 return 'rt-risk-low';
387 }
388
389
390 /**
391 * Assess the risk associated with low ratings or poor compatibility feedback, return corresponding CSS class
392 */
393 public function get_timediff_risk_classname( $time_diff ) {
394 $days = $time_diff / ( DAY_IN_SECONDS );
395 if ( $days > 365 ) {
396 return 'rt-risk-high';
397 }
398 if ( $days > 90 ) {
399 return 'rt-risk-medium';
400 }
401 return 'rt-risk-low';
402 }
403
404
405 /**
406 * Get the latest available WordPress version using WP core functions
407 * This way, we don't need to do any API calls. WP check this periodically anyway.
408 */
409 public function check_core_updates() {
410 global $wp_version;
411 $update = get_preferred_from_update_core();
412 // Bail out of no valid response, or false.
413 if ( ! $update || $update == false ) {
414 return $wp_version;
415 }
416 // If latest, return current version number.
417 if ( $update->response == 'latest' ) {
418 return $wp_version;
419 }
420 // Return the preferred update's version number.
421 return $update->version;
422 }
423
424
425 /**
426 * Interpret the 'compatibility' info from the repo, and return a summary string
427 */
428 public function format_compatibility( $compatibility ) {
429 if ( empty( $compatibility ) || ! is_array( $compatibility ) ) {
430 return esc_html__( 'No data available', 'plugin-report' );
431 }
432 // get the latest WP release version number
433 $wp_latest = $this->check_core_updates();
434
435 $out = '';
436 $score = array();
437 foreach ( $compatibility as $pluginversion => $data ) {
438 $out .= $pluginversion . ': ' . $data[0] . '% (' . $data[2] . '/' . $data[1] . ')';
439 $score[] = intval( $data[0] );
440 }
441 $css_class = $this->get_percentage_risk_classname( $score[0] );
442 return '<span class="' . $css_class . '">' . $out . '</span>';
443
444 }
445
446
447 /**
448 * Extract the major release number from a WP version nr
449 */
450 public function major_release_version_nr( $version ) {
451 $parts = explode( '.', $version );
452 $parts = array_slice( $parts, 0, 2 );
453 return implode( '.', $parts );
454 }
455
456
457 /**
458 * Clear all cached plugin info
459 */
460 public function clear_cache() {
461 $plugins = get_plugins();
462 foreach ( $plugins as $key => $plugin ) {
463 $slug = $this->get_plugin_slug( $key );
464 $cache_key = 'rt_plugin_report_cache_' . $slug;
465 delete_site_transient( $cache_key );
466 }
467 }
468
469 }
470
471 // Instantiate the class.
472 $rt_plugin_report_instance = new RT_Plugin_Report();
473 $rt_plugin_report_instance->init();
474
475 }
476