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

783 lines 29.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://wordpress.org/plugins/plugin-report/
5 * Description: Provides detailed information about currently installed plugins
6 * Version: 2.2.1
7 * Requires at least: 4.6
8 * Requires PHP: 5.6
9 * Author: Torsten Landsiedel
10 * Author URI: https://torstenlandsiedel.de
11 * License: GPLv3
12 * Network: true
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 /**
24 * Plugin Report main class.
25 */
26 class RT_Plugin_Report {
27
28 // CSS class constants.
29 const CSS_CLASS_LOW = 'pr-risk-low';
30 const CSS_CLASS_MED = 'pr-risk-medium';
31 const CSS_CLASS_HIGH = 'pr-risk-high';
32
33 // Other class constants.
34 const PLUGIN_VERSION = '2.2.0';
35 const COLS_PER_ROW = 9;
36 const CACHE_LIFETIME = DAY_IN_SECONDS;
37 const CACHE_LIFETIME_NOREPO = WEEK_IN_SECONDS;
38
39 /**
40 * Constructor
41 */
42 public function __construct() {
43 // Intentionally left blank.
44 }
45
46
47 /**
48 * Set up things like hooks and such
49 */
50 public function init() {
51 // Hook for the admin page.
52 if ( is_multisite() ) {
53 add_action( 'network_admin_menu', array( $this, 'register_settings_page' ) );
54 } else {
55 add_action( 'admin_menu', array( $this, 'register_settings_page' ) );
56 }
57 // Hook for the admin js.
58 add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) );
59 // Add the AJAX hook.
60 add_action( 'wp_ajax_rt_get_plugin_info', array( $this, 'get_plugin_info' ) );
61 // Hook into the WP Upgrader to selectively delete cache items.
62 add_action( 'upgrader_process_complete', array( $this, 'upgrade_delete_cache_items' ), 10, 2 );
63 }
64
65
66 /**
67 * Add a new options page to the network admin
68 */
69 public function register_settings_page() {
70 add_plugins_page(
71 esc_html_x( 'Plugin Report', 'Page and menu title', 'plugin-report' ),
72 esc_html_x( 'Plugin Report', 'Page and menu title', 'plugin-report' ),
73 is_multisite() ? 'manage_sites' : 'manage_options',
74 'plugin_report',
75 array( $this, 'settings_page' )
76 );
77 }
78
79
80 /**
81 * Render the options page
82 */
83 public function settings_page() {
84 // Check user capabilities, just to be sure.
85 if ( ! current_user_can( 'manage_options' ) ) {
86 wp_die();
87 }
88 // Assemble information we'll need.
89 global $wp_version;
90 $plugins = get_plugins();
91
92 // Check wether a core update is available.
93 $wp_latest = $this->check_core_updates();
94
95 // 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).
96 if ( isset( $_GET['clear_cache'] ) ) {
97 $new_timestamp = intval( $_GET['clear_cache'] );
98 $last_timestamp = intval( get_site_transient( 'plugin_report_cache_cleared' ) );
99 if ( ! $last_timestamp || $new_timestamp > $last_timestamp ) {
100 $this->clear_cache();
101 set_site_transient( 'plugin_report_cache_cleared', $new_timestamp, self::CACHE_LIFETIME );
102 }
103 }
104
105 // Start the page's output.
106 echo '<div class="wrap">';
107 echo '<h1>' . esc_html_x( 'Plugin Report', 'Page and menu title', 'plugin-report' ) . '</h1>';
108 echo '<p>';
109 $version_temp = '<span class="' . $this->get_version_risk_classname( $wp_version, $wp_latest ) . '">' . $wp_version . '</span>';
110 /* translators: %1$s: Current WordPress version number, %2$s: Current PHP version number */
111 echo sprintf( esc_html__( 'Currently running WordPress version %1$s and PHP version %2$s.', 'plugin-report' ), $version_temp, phpversion() );
112 if ( version_compare( $wp_version, $wp_latest, '<' ) ) {
113 /* translators: %s = Available new version number */
114 echo sprintf( ' (' . esc_html__( 'An upgrade to %s is available', 'plugin-report' ) . ')', $wp_latest );
115 }
116 echo '</p>';
117 echo '<p>';
118 // Clear cache and reload.
119 if ( is_multisite() ) {
120 $page_url = 'network/plugins.php?page=plugin_report';
121 } else {
122 $page_url = 'plugins.php?page=plugin_report';
123 }
124 echo '<a href="' . esc_attr( admin_url( $page_url . '&clear_cache=' . current_time( 'timestamp' ) ) ) . '">' . esc_html__( 'Clear cached plugin data and reload', 'plugin-report' ) . '</a>';
125 echo '</p>';
126 echo '<h2>' . esc_html__( 'Currently installed plugins', 'plugin-report' ) . '</h2>';
127 echo '<p id="plugin-report-progress"></p>';
128
129 // The report's main table.
130 echo '<table id="plugin-report-table" class="wp-list-table widefat fixed striped">';
131 echo '<thead>';
132 echo '<tr>';
133 echo '<th data-sort-default>' . esc_html__( 'Name', 'plugin-report' ) . '</th>';
134 echo '<th>' . esc_html__( 'Author', 'plugin-report' ) . '</th>';
135 echo '<th>' . esc_html__( 'Repository', 'plugin-report' ) . '</th>';
136 echo '<th>' . esc_html__( 'Activated', 'plugin-report' ) . '</th>';
137 echo '<th data-sort-method="none" class="no-sort">' . esc_html__( 'Installed version', 'plugin-report' ) . '</th>';
138 echo '<th>' . esc_html__( 'Auto-update', 'plugin-report' ) . '</th>';
139 echo '<th>' . esc_html__( 'Last update', 'plugin-report' ) . '</th>';
140 echo '<th data-sort-method="dotsep">' . esc_html__( 'Tested up to WP version', 'plugin-report' ) . '</th>';
141 echo '<th data-sort-method="number">' . esc_html__( 'Rating', 'plugin-report' ) . '</th>';
142 echo '</tr>';
143 echo '</thead>';
144 echo '<tbody>';
145
146 foreach ( $plugins as $key => $plugin ) {
147 $slug = $this->get_plugin_slug( $key );
148 $cache_key = $this->create_cache_key( $slug );
149 $cache = get_site_transient( $cache_key );
150 if ( $cache ) {
151 // Use the cached report to create a table row.
152 echo $this->render_table_row( $cache );
153 } else {
154 // Render a special table row that's used as a signal to the front-end js that new data is needed.
155 echo '<tr class="plugin-report-row-temp-' . esc_attr( $slug ) . '"><td colspan="' . (int) self::COLS_PER_ROW . '">' . esc_html__( 'Loading...', 'plugin-report' ) . '</td></tr>';
156 }
157 }
158
159 echo '</tbody>';
160 echo '</table>';
161
162 echo '<p id="plugin-report-buttons"></p>';
163
164 // Wrap up.
165 echo '</div>';
166 }
167
168
169 /**
170 * Enqueue admin javascript
171 *
172 * @param string $hook Screen hook.
173 */
174 public function enqueue_assets( $hook ) {
175 // Check if we're on the right screen.
176 if ( 'plugins_page_plugin_report' !== $hook ) {
177 return;
178 }
179 // Register the plugin's admin js, and require jquery.
180 wp_enqueue_script( 'plugin-report-js', plugins_url( '/js/plugin-report.js', __FILE__ ), array( 'jquery', 'plugin-report-tablesort-js' ), self::PLUGIN_VERSION );
181 wp_enqueue_script( 'plugin-report-tablesort-js', plugins_url( '/js/tablesort.min.js', __FILE__ ), array( 'jquery' ), '5.6.0' );
182 wp_enqueue_script( 'plugin-report-tablesort-number-js', plugins_url( '/js/tablesort.number.min.js', __FILE__ ), array( 'plugin-report-tablesort-js' ), '5.6.0' );
183 wp_enqueue_script( 'plugin-report-tablesort-dotsep-js', plugins_url( '/js/tablesort.dotsep.min.js', __FILE__ ), array( 'plugin-report-tablesort-js' ), '5.6.0' );
184 // Add some variables to the page, to be used by the javascript.
185 $slugs = $this->get_plugin_slugs();
186 $slugs_str = implode( ',', $slugs );
187 $vars = array(
188 'plugin_slugs' => $slugs_str,
189 'ajax_nonce' => wp_create_nonce( 'plugin_report_nonce' ),
190 'export_btn' => __( 'Export .csv file', 'plugin-report' ),
191 'plugin_url_header' => __( 'Plugin URL', 'plugin-report' ),
192 'author_url_header' => __( 'Author URL', 'plugin-report' ),
193 );
194 wp_localize_script( 'plugin-report-js', 'plugin_report_vars', $vars );
195 // Enqueue admin CSS file.
196 wp_enqueue_style( 'plugin-report-css', plugin_dir_url( __FILE__ ) . 'css/plugin-report.css', array(), self::PLUGIN_VERSION );
197 }
198
199
200 /**
201 * Get the slugs for all currently installed plugins
202 */
203 private function get_plugin_slugs() {
204 $plugins = get_plugins();
205 $slugs = array();
206 foreach ( $plugins as $key => $plugin ) {
207 $slugs[] = $this->get_plugin_slug( $key );
208 }
209 return $slugs;
210 }
211
212
213 /**
214 * Convert a plugin's file path into its slug.
215 *
216 * @param string $file Plugin file path.
217 */
218 private function get_plugin_slug( $file ) {
219 if ( strpos( $file, '/' ) !== false ) {
220 $parts = explode( '/', $file );
221 } else {
222 $parts = explode( '.', $file );
223 }
224 return sanitize_title( $parts[0] );
225 }
226
227
228 /**
229 * AJAX handler
230 * Returns a full html table row with the plugin's data
231 */
232 public function get_plugin_info() {
233
234 // Check the ajax nonce, display an error if the check fails.
235 if ( ! check_ajax_referer( 'plugin_report_nonce', 'nonce', false ) ) {
236 wp_die();
237 }
238
239 // Check user capabilites, just to be sure.
240 if ( ! current_user_can( 'manage_options' ) ) {
241 wp_die();
242 }
243
244 // Check if get_plugins() function exists.
245 if ( ! function_exists( 'plugins_api' ) ) {
246 require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
247 }
248
249 if ( isset( $_POST['slug'] ) ) {
250 $slug = sanitize_title( wp_unslash( $_POST['slug'] ) );
251 } else {
252 $slug = ''; // Set value to an empty string.
253 }
254
255 $report = $this->assemble_plugin_report( $slug );
256
257 if ( $report ) {
258 $table_row = $this->render_table_row( $report );
259 } else {
260 $table_row = $this->render_error_row( esc_html__( 'No plugin data available.', 'plugin-report' ) );
261 }
262
263 // Formulate a response.
264 $response = array(
265 'html' => $table_row,
266 'message' => 'Success!',
267 );
268 // Return the response.
269 echo wp_json_encode( $response );
270
271 wp_die();
272 }
273
274
275 /**
276 * Gather all the info we can get about a plugin.
277 * Uses transient caching to avoid doing repo API calls on every page visit.
278 *
279 * @param string $slug Plugin slug.
280 */
281 private function assemble_plugin_report( $slug ) {
282 if ( ! empty( $slug ) ) {
283 $report = array();
284 $cache_key = $this->create_cache_key( $slug );
285 $cache = get_site_transient( $cache_key );
286 $plugins = get_plugins();
287 $auto_updates = (array) get_site_option( 'auto_update_plugins', array() );
288
289 if ( empty( $cache ) ) {
290
291 // Add the plugin's slug to the report.
292 $report['slug'] = $slug;
293
294 // Get the locally available info, and add it to the report.
295 foreach ( $plugins as $key => $plugin ) {
296 if ( $this->get_plugin_slug( $key ) === $slug ) {
297
298 // Translate plugin data.
299 $textdomain = $plugin['TextDomain'];
300 if ( $textdomain ) {
301 if ( ! is_textdomain_loaded( $textdomain ) ) {
302 if ( $plugin['DomainPath'] ) {
303 load_plugin_textdomain( $textdomain, false, dirname( $key ) . $plugin['DomainPath'] );
304 } else {
305 load_plugin_textdomain( $textdomain, false, dirname( $key ) );
306 }
307 }
308 } elseif ( 'hello.php' === basename( $key ) ) {
309 $textdomain = 'default';
310 }
311 if ( $textdomain ) {
312 foreach ( array( 'Name', 'PluginURI', 'Description', 'Author', 'AuthorURI', 'Version' ) as $field ) {
313 // phpcs:ignore WordPress.WP.I18n.LowLevelTranslationFunction,WordPress.WP.I18n.NonSingularStringLiteralText,WordPress.WP.I18n.NonSingularStringLiteralDomain
314 $plugin[ $field ] = translate( $plugin[ $field ], $textdomain );
315 }
316 }
317
318 $report['local_info'] = $plugin;
319 $report['file_path'] = $key;
320 $report['auto-update'] = in_array( $key, $auto_updates, true );
321
322 // Change any whitespace to default space.
323 $report['local_info']['Name'] = preg_replace( '/\s+/u', ' ', $report['local_info']['Name'] );
324
325 break;
326 }
327 }
328
329 // Use the wordpress.org repository API to get detailed information.
330 $args = array(
331 'slug' => $slug,
332 'fields' => array(
333 'description' => false,
334 'sections' => false,
335 'tags' => false,
336 'version' => true,
337 'tested' => true,
338 'requires' => true,
339 'requires_php' => true,
340 'compatibility' => true,
341 'author' => true,
342 ),
343 );
344
345 // Check wordpress.org only if "Update URI" plugin header is not set or set to wordpress.org.
346 $parsed_repo_url = wp_parse_url( $report['local_info']['UpdateURI'] );
347 $repo_host = isset( $parsed_repo_url['host'] ) ? $parsed_repo_url['host'] : null;
348 if ( empty( $repo_host ) || strtolower( $repo_host ) === 'w.org' || strtolower( $repo_host ) === 'wordpress.org' ) {
349 $returned_object = plugins_api( 'plugin_information', $args );
350 }
351
352 // Add the repo info to the report.
353 if ( isset( $returned_object ) ) {
354 if ( ! is_wp_error( $returned_object ) ) {
355 $report['repo_info'] = $returned_object;
356 // Cache the report.
357 set_site_transient( $cache_key, $report, self::CACHE_LIFETIME );
358 } else {
359 // Store the error code and message in the report.
360 $report['repo_error_code'] = $returned_object->get_error_code();
361 $report['repo_error_message'] = $returned_object->get_error_message();
362 // Because the plugin is not found in the wordpress.org repo, check if it exists in SVN.
363 $report['exists_in_svn'] = $this->check_exists_in_svn( $slug );
364 // Cache for an extra long time when the plugin is not in the repo.
365 set_site_transient( $cache_key, $report, self::CACHE_LIFETIME_NOREPO );
366 }
367 }
368 } else {
369 $report = $cache;
370 }
371
372 return $report;
373
374 } else {
375 return null;
376 }
377
378 }
379
380
381 /**
382 * Check if the plugin is present in WordPress's SVN repository.
383 *
384 * Function adapted from the 'Enhanced Plugin Admin' plugin by Marios Alexandrou.
385 * See: https://plugins.trac.wordpress.org/browser/enhanced-plugin-admin/trunk/enhanced-plugin-admin.php
386 *
387 * @param string $slug The plugin's slug.
388 *
389 * @return boolean True if found, false if not.
390 */
391 private function check_exists_in_svn( $slug ) {
392 // Attempt to load the plugin's SVN repo page.
393 $response = wp_remote_get( 'http://svn.wp-plugins.org/' . $slug . '/' );
394 // If the return value was a WP_Error, assume the answer is no.
395 if ( is_wp_error( $response ) ) {
396 return false;
397 } else {
398 // If the returned HTTP code is 200, the page was found, so return true.
399 $response_code = wp_remote_retrieve_response_code( $response );
400 if ( 200 === $response_code ) {
401 return true;
402 }
403 }
404 // In all other cases, assume the plugin was not found.
405 return false;
406 }
407
408
409 /**
410 * From a report, generate an HTML table row with relevant data for the plugin.
411 *
412 * @param array|false $report Report of plugin.
413 */
414 private function render_table_row( $report ) {
415 // Get the current WP version number.
416 global $wp_version;
417 // Get the latest WP release version number.
418 $wp_latest = $this->check_core_updates();
419 // Check if the report is valid.
420 if ( false === $report ) {
421 $html = $this->render_error_row( esc_html__( 'No plugin data available.', 'plugin-report' ) );
422 } else {
423 // Start the new table row.
424 $html = '<tr class="plugin-report-row-' . $report['slug'] . '">';
425
426 // Name.
427 if ( isset( $report['local_info']['PluginURI'] ) && ! empty( $report['local_info']['PluginURI'] ) ) {
428 $html .= '<td><a href="' . $report['local_info']['PluginURI'] . '"><strong>' . $report['local_info']['Name'] . '</strong></a></td>';
429 } else {
430 $html .= '<td><strong>' . $report['local_info']['Name'] . '</strong></td>';
431 }
432
433 // Author.
434 if ( isset( $report['local_info']['AuthorURI'] ) && ! empty( $report['local_info']['AuthorURI'] ) ) {
435 $html .= '<td><a href="' . $report['local_info']['AuthorURI'] . '">' . $report['local_info']['Author'] . '</a></td>';
436 } else {
437 $html .= '<td>' . $report['local_info']['Author'] . '</td>';
438 }
439
440 // Repository.
441 if ( isset( $report['local_info']['UpdateURI'] ) ) {
442 // Parse the UpdateURI's value to get the host.
443 $parsed_repo_url = wp_parse_url( $report['local_info']['UpdateURI'] );
444 // If the URI is valid, extract the host, otherwise we'll use the header value.
445 $repo_host = isset( $parsed_repo_url['host'] ) ? $parsed_repo_url['host'] : $report['local_info']['UpdateURI'];
446 // Check if the plugin is supposed to be hosted on wp.org.
447 if ( empty( $repo_host ) || strtolower( $repo_host ) === 'w.org' || strtolower( $repo_host ) === 'wordpress.org' ) {
448 // Plugin should be available on wp.org, check if we got a 'not found' error.
449 if ( isset( $report['repo_error_code'] ) && $report['repo_error_code'] === 'plugins_api_failed' ) {
450 // Plugin is not available in the wp.org repo.
451 if ( isset( $report['exists_in_svn'] ) && $report['exists_in_svn'] === true ) {
452 $html .= '<td class="' . self::CSS_CLASS_HIGH . '">' . __( 'wordpress.org, plugin closed', 'plugin-report' ) . '</td>';
453 } else {
454 $html .= '<td class="' . self::CSS_CLASS_HIGH . '">' . __( 'wordpress.org, plugin not found', 'plugin-report' ) . '</td>';
455 }
456 } else {
457 // Plugin is available on wp.org.
458 $html .= '<td class="' . self::CSS_CLASS_LOW . '">wordpress.org</td>';
459 }
460 } else {
461 if ( $parsed_repo_url && isset( $parsed_repo_url['host'] ) ) {
462 // Update URI is a valid URL, display the host.
463 $html .= '<td class="' . self::CSS_CLASS_MED . '">' . $repo_host . '</td>';
464 } else {
465 // Some other value (like 'false'), so assume updates are disabled.
466 $html .= '<td class="' . self::CSS_CLASS_MED . '">' . __( 'Updates disabled', 'plugin-report' ) . '</td>';
467 }
468 }
469 } elseif ( version_compare( $wp_version, '5.8', '<' ) ) {
470 $html .= $this->render_error_cell( esc_html__( 'Only available in WP 5.8+', 'plugin-report' ) );
471 } else {
472 $html .= $this->render_error_cell();
473 }
474
475 // Activated.
476 $active = __( 'Please clear cache to update', 'plugin-report' );
477 $css_class = self::CSS_CLASS_MED;
478 if ( is_multisite() ) {
479 $activation_status = $this->get_multisite_activation( $report['file_path'] );
480 if ( true === $activation_status['network'] ) {
481 $css_class = self::CSS_CLASS_LOW;
482 $html .= '<td class="' . $css_class . '">' . __( 'Network activated', 'plugin-report' ) . '</td>';
483 } else {
484 $css_class = ( $activation_status['active'] > 0 ) ? self::CSS_CLASS_LOW : self::CSS_CLASS_HIGH;
485 $html .= '<td class="' . $css_class . '">' . $activation_status['active'] . '/' . $activation_status['sites'] . '</td>';
486 }
487 } else {
488 if ( isset( $report['file_path'] ) ) {
489 $active = is_plugin_active( $report['file_path'] ) ? __( 'Yes', 'plugin-report' ) : __( 'No', 'plugin-report' );
490 $css_class = is_plugin_active( $report['file_path'] ) ? self::CSS_CLASS_LOW : self::CSS_CLASS_HIGH;
491 }
492 $html .= '<td class="' . $css_class . '">' . $active . '</td>';
493 }
494
495 // Installed / available version.
496 if ( isset( $report['repo_info'] ) ) {
497 $css_class = $this->get_version_risk_classname( $report['local_info']['Version'], $report['repo_info']->version );
498 $html .= '<td class="' . $css_class . '">';
499 $html .= $report['local_info']['Version'];
500 if ( $report['local_info']['Version'] !== $report['repo_info']->version ) {
501 // Any platform upgrades needed?
502 $needs_php_upgrade = isset( $report['repo_info']->requires_php ) ? version_compare( phpversion(), $report['repo_info']->requires_php, '<' ) : false;
503 $needs_wp_upgrade = isset( $report['repo_info']->requires ) ? version_compare( $wp_version, $report['repo_info']->requires, '<' ) : false;
504 // Create the additional message.
505 if ( $needs_wp_upgrade && $needs_php_upgrade ) {
506 /* translators: %1$s: Plugin version number, %2$s: WP version number, %3$s: PHP version number */
507 $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>';
508 } elseif ( $needs_wp_upgrade ) {
509 /* translators: %1$s: Plugin version number, %2$s: WP version number. */
510 $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>';
511 } elseif ( $needs_php_upgrade ) {
512 /* translators: %1$s: Plugin version number, %2$s: PHP version number. */
513 $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>';
514 } else {
515 /* translators: %s: Plugin version number. */
516 $html .= ' <span class="pr-additional-info">' . sprintf( esc_html__( '(%s available)', 'plugin-report' ), $report['repo_info']->version ) . '</span>';
517 }
518 }
519 $html .= '</td>';
520 } else {
521 $html .= '<td>' . $report['local_info']['Version'] . '</td>';
522 }
523
524 // Auto-update.
525 if ( version_compare( $wp_version, '5.5', '<' ) ) {
526 $html .= '<td>' . __( 'Requires WordPress 5.5 or higher', 'plugin-report' ) . '</td>';
527 } else {
528 if ( isset( $report['auto-update'] ) && $report['auto-update'] ) {
529 $html .= '<td class="' . self::CSS_CLASS_LOW . '">' . __( 'Enabled', 'plugin-report' ) . '</td>';
530 } else {
531 $html .= '<td>' . __( 'Not enabled', 'plugin-report' ) . '</td>';
532 }
533 }
534
535 // Last updates.
536 if ( isset( $report['repo_info'] ) && isset( $report['repo_info']->last_updated ) ) {
537 $time_update = new DateTime( $report['repo_info']->last_updated );
538 $time_diff = human_time_diff( $time_update->getTimestamp(), current_time( 'timestamp' ) );
539 $css_class = $this->get_timediff_risk_classname( current_time( 'timestamp' ) - $time_update->getTimestamp() );
540 $html .= '<td class="' . $css_class . '" data-sort="' . $time_update->getTimestamp() . '">' . $time_diff . '</td>';
541 } else {
542 $html .= $this->render_error_cell();
543 }
544
545 // Tested up to.
546 if ( isset( $report['repo_info'] ) && isset( $report['repo_info']->tested ) && ! empty( $report['repo_info']->tested ) ) {
547 $css_class = $this->get_version_risk_classname( $report['repo_info']->tested, $wp_latest, true );
548 $html .= '<td class="' . $css_class . '">' . $report['repo_info']->tested . '</td>';
549 } else {
550 $html .= $this->render_error_cell();
551 }
552
553 // Overall user rating.
554 if ( isset( $report['repo_info'] ) && isset( $report['repo_info']->num_ratings ) && isset( $report['repo_info']->rating ) ) {
555 $css_class = ( intval( $report['repo_info']->num_ratings ) > 0 ) ? $this->get_percentage_risk_classname( intval( $report['repo_info']->rating ) ) : '';
556 $value_text = ( ( intval( $report['repo_info']->num_ratings ) > 0 ) ? $report['repo_info']->rating . '%' : esc_html__( 'No data available', 'plugin-report' ) );
557 $html .= '<td class="' . $css_class . '">' . $value_text . '</td>';
558 } else {
559 $html .= $this->render_error_cell();
560 }
561
562 // Close the new table row.
563 $html .= '</tr>';
564 }
565 return $html;
566 }
567
568
569 /**
570 * Format an error message as a table row, so we can return it to javascript.
571 *
572 * @param string $message Message to be shown.
573 */
574 private function render_error_row( $message ) {
575 return '<tr class="pluginreport-row-error"><td colspan="' . self::COLS_PER_ROW . '">' . $message . '</td></tr>';
576 }
577
578
579 /**
580 * Format an error message as a table cell, so we can return it to javascript.
581 *
582 * @param string $message Message to be shown.
583 */
584 private function render_error_cell( $message = null ) {
585 if ( ! $message ) {
586 $message = esc_html__( 'No data available', 'plugin-report' );
587 }
588 return '<td class="pluginreport-cell-error" data-sort="0">' . $message . '</td>';
589 }
590
591
592 /**
593 * Return the version string with all elements beyond the second removed ("5.5.1" -> "5.5").
594 *
595 * @param string $version_string Complete version number.
596 */
597 private function get_major_version( $version_string ) {
598 $parts = explode( '.', $version_string );
599 array_splice( $parts, 2 );
600 return implode( '.', $parts );
601 }
602
603
604 /**
605 * Figure out what CSS class to use based on current and optimal version numbers.
606 *
607 * @param string $available Available version.
608 * @param string $optimal Optimal version.
609 * @param bool $major_only True to compare only major versions, false otherwise.
610 */
611 private function get_version_risk_classname( $available, $optimal, $major_only = false ) {
612 // Use only the first two elements of the version number if $major_only is set to true.
613 // This is used for WP version numbers, where point releases are not considered a risk.
614 if ( $major_only ) {
615 $available = $this->get_major_version( $available );
616 $optimal = $this->get_major_version( $optimal );
617 }
618 // If the version is equal or higher, indicate low risk.
619 if ( version_compare( $available, $optimal, '>=' ) ) {
620 return self::CSS_CLASS_LOW;
621 }
622 // Else, indicate high risk.
623 return self::CSS_CLASS_HIGH;
624 }
625
626
627 /**
628 * Assess the risk associated with low ratings or poor compatibility feedback, return corresponding CSS class.
629 *
630 * @param int $perc Rating percentage.
631 */
632 private function get_percentage_risk_classname( $perc ) {
633 if ( $perc < 70 ) {
634 return self::CSS_CLASS_HIGH;
635 }
636 if ( $perc < 90 ) {
637 return self::CSS_CLASS_MED;
638 }
639 return self::CSS_CLASS_LOW;
640 }
641
642
643 /**
644 * Assess the risk associated with low ratings or poor compatibility feedback, return corresponding CSS class.
645 *
646 * @param int $time_diff Time difference in seconds.
647 */
648 private function get_timediff_risk_classname( $time_diff ) {
649 $days = $time_diff / ( DAY_IN_SECONDS );
650 if ( $days > 365 ) {
651 return self::CSS_CLASS_HIGH;
652 }
653 if ( $days > 90 ) {
654 return self::CSS_CLASS_MED;
655 }
656 return self::CSS_CLASS_LOW;
657 }
658
659
660 /**
661 * Get the latest available WordPress version using WP core functions
662 * This way, we don't need to do any API calls. WP check this periodically anyway.
663 */
664 private function check_core_updates() {
665 global $wp_version;
666 $update = get_preferred_from_update_core();
667 // Bail out of no valid response, or false.
668 if ( false === $update ) {
669 return $wp_version;
670 }
671 // If latest, return current version number.
672 if ( is_object( $update ) && 'latest' === $update->response ) {
673 return $wp_version;
674 }
675 // Return the preferred update's version number.
676 return is_object( $update ) ? $update->version : $update['version'];
677 }
678
679
680 /**
681 * Gather statistics about a plugin's activation on a multisite install.
682 *
683 * @param string $path Plugin path.
684 */
685 private function get_multisite_activation( $path ) {
686 // Create an array to contain the return values.
687 $activation_status = array(
688 'network' => false,
689 'active' => 0,
690 'sites' => 1,
691 );
692 // Check if the plugin is network activated.
693 $network_plugins = get_site_option( 'active_sitewide_plugins', null );
694 if ( array_key_exists( $path, $network_plugins ) ) {
695 $activation_status['network'] = true;
696 } else {
697 // Get a list of all sites in the multisite install.
698 $args = array(
699 'number' => 9999,
700 'fields' => 'ids',
701 );
702 $sites = get_sites( $args );
703 // Add the total number of sites to the return array.
704 $activation_status['sites'] = count( $sites );
705 // Loop through the sites to find where the plugin is active.
706 foreach ( $sites as $site_id ) {
707 $plugins = get_blog_option( $site_id, 'active_plugins', null );
708 if ( $plugins ) {
709 foreach ( $plugins as $plugin_path ) {
710 if ( $plugin_path === $path ) {
711 $activation_status['active']++;
712 }
713 }
714 }
715 }
716 }
717 // Return the data we gathered.
718 return $activation_status;
719 }
720
721
722 /**
723 * Create a cache key that is unique to the provided plugin slug.
724 *
725 * @param string $slug Plugin slug.
726 */
727 private function create_cache_key( $slug ) {
728 // Create a hash for the plugin slug.
729 $slug_hash = hash( 'sha256', $slug );
730 // Prefix and limit the string to 40 characters to avoid issues with long keys.
731 $cache_key = 'rtpr_' . substr( $slug_hash, 0, 35 );
732 // Return the key.
733 return $cache_key;
734 }
735
736
737 /**
738 * Clear all cached plugin info
739 */
740 private function clear_cache() {
741 // Request a list of all plugins.
742 $plugins = get_plugins();
743 // Loop through the plugins array, and delete cache items.
744 foreach ( $plugins as $key => $plugin ) {
745 $slug = $this->get_plugin_slug( $key );
746 $this->clear_cache_item( $slug );
747 }
748 }
749
750
751 /**
752 * Remove the cache item for a single plugin.
753 *
754 * @param string $slug Plugin slug.
755 */
756 private function clear_cache_item( $slug ) {
757 $cache_key = $this->create_cache_key( $slug );
758 delete_site_transient( $cache_key );
759 }
760
761
762 /**
763 * Selectively delete cache for plugins that have been updated.
764 */
765 public function upgrade_delete_cache_items( $upgrader, $data ) {
766 // Check if plugins have been upgraded by WP.
767 if ( isset( $data ) && isset( $data['plugins'] ) && is_array( $data['plugins'] ) ) {
768 // Loop through the plugins, and delete the associated cache items.
769 foreach ( $data['plugins'] as $key => $value ) {
770 $slug = $this->get_plugin_slug( $value );
771 $this->clear_cache_item( $slug );
772 }
773 }
774 }
775
776 }
777
778 // Instantiate the class.
779 $plugin_report_instance = new RT_Plugin_Report();
780 $plugin_report_instance->init();
781
782 }
783