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

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