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

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