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

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