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

747 lines 27.9 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.2
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 = '2.0.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.3' );
184 wp_enqueue_script( 'plugin-report-tablesort-number-js', plugins_url( '/js/tablesort.number.min.js', __FILE__ ), array( 'plugin-report-tablesort-js' ), '5.3' );
185 wp_enqueue_script( 'plugin-report-tablesort-dotsep-js', plugins_url( '/js/tablesort.dotsep.min.js', __FILE__ ), array( 'plugin-report-tablesort-js' ), '5.3' );
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( $key ) . $plugin['DomainPath'] );
302 } else {
303 load_plugin_textdomain( $textdomain, false, dirname( $key ) );
304 }
305 }
306 } elseif ( 'hello.php' === basename( $key ) ) {
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 ( isset( $returned_object ) ) {
352 if ( ! is_wp_error( $returned_object ) ) {
353 $report['repo_info'] = maybe_unserialize( $returned_object );
354 // Cache the report.
355 set_site_transient( $cache_key, $report, self::CACHE_LIFETIME );
356 } else {
357 // Store the error code and message in the report.
358 $report['repo_error_code'] = $returned_object->get_error_code();
359 $report['repo_error_message'] = $returned_object->get_error_message();
360 // Cache for an extra long time when the plugin is not in the repo.
361 set_site_transient( $cache_key, $report, self::CACHE_LIFETIME_NOREPO );
362 }
363 }
364 } else {
365 $report = $cache;
366 }
367
368 return $report;
369
370 } else {
371 return null;
372 }
373
374 }
375
376
377 /**
378 * From a report, generate an HTML table row with relevant data for the plugin.
379 *
380 * @param array $report Report of plugin.
381 */
382 private function render_table_row( $report ) {
383 // Get the current WP version number.
384 global $wp_version;
385 // Get the latest WP release version number.
386 $wp_latest = $this->check_core_updates();
387 // Check if the report is valid.
388 if ( null === $report ) {
389 $html = $this->render_error_row( esc_html__( 'No plugin data available.', 'plugin-report' ) );
390 } else {
391 // Start the new table row.
392 $html = '<tr class="plugin-report-row-' . $report['slug'] . '">';
393
394 // Name.
395 if ( isset( $report['local_info']['PluginURI'] ) && ! empty( $report['local_info']['PluginURI'] ) ) {
396 $html .= '<td><a href="' . $report['local_info']['PluginURI'] . '"><strong>' . $report['local_info']['Name'] . '</strong></a></td>';
397 } else {
398 $html .= '<td><strong>' . $report['local_info']['Name'] . '</strong></td>';
399 }
400
401 // Author.
402 if ( isset( $report['local_info']['AuthorURI'] ) && ! empty( $report['local_info']['AuthorURI'] ) ) {
403 $html .= '<td><a href="' . $report['local_info']['AuthorURI'] . '">' . $report['local_info']['Author'] . '</a></td>';
404 } else {
405 $html .= '<td>' . $report['local_info']['Author'] . '</td>';
406 }
407
408 // Repository.
409 if ( isset( $report['local_info']['UpdateURI'] ) ) {
410 // Parse the UpdateURI's value to get the host.
411 $parsed_repo_url = wp_parse_url( $report['local_info']['UpdateURI'] );
412 // If the URI is valid, extract the host, otherwise we'll use the header value.
413 $repo_host = isset( $parsed_repo_url['host'] ) ? $parsed_repo_url['host'] : $report['local_info']['UpdateURI'];
414 // Check if the plugin is supposed to be hosted on wp.org.
415 if ( empty( $repo_host ) || strtolower( $repo_host ) === 'w.org' || strtolower( $repo_host ) === 'wordpress.org' ) {
416 // Plugin should be available on wp.org, check if we got a 'not found' error.
417 if ( isset( $report['repo_error_code'] ) && $report['repo_error_code'] === 'plugins_api_failed' ) {
418 // Plugin is not available in the wp.org repo.
419 $html .= '<td class="' . self::CSS_CLASS_HIGH . '">' . __( 'wordpress.org, plugin not found', 'plugin-report' ) . '</td>';
420 } else {
421 // Plugin is available on wp.org.
422 $html .= '<td class="' . self::CSS_CLASS_LOW . '">wordpress.org</td>';
423 }
424 } else {
425 if ( $parsed_repo_url && isset( $parsed_repo_url[ 'host' ] ) ) {
426 // Update URI is a valid URL, display the host.
427 $html .= '<td class="' . self::CSS_CLASS_MED . '">' . $repo_host . '</td>';
428 } else {
429 // Some other value (like 'false'), so assume updates are disabled.
430 $html .= '<td class="' . self::CSS_CLASS_MED . '">' . __( 'Updates disabled', 'plugin-report' ) . '</td>';
431 }
432 }
433 } else {
434 $html .= $this->render_error_cell();
435 }
436
437 // Activated.
438 $active = __( 'Please clear cache to update', 'plugin-report' );
439 $css_class = self::CSS_CLASS_MED;
440 if ( is_multisite() ) {
441 $activation_status = $this->get_multisite_activation( $report['file_path'] );
442 if ( true === $activation_status['network'] ) {
443 $css_class = self::CSS_CLASS_LOW;
444 $html .= '<td class="' . $css_class . '">' . __( 'Network activated', 'plugin-report' ) . '</td>';
445 } else {
446 $css_class = ( $activation_status['active'] > 0 ) ? self::CSS_CLASS_LOW : self::CSS_CLASS_HIGH;
447 $html .= '<td class="' . $css_class . '">' . $activation_status['active'] . '/' . $activation_status['sites'] . '</td>';
448 }
449 } else {
450 if ( isset( $report['file_path'] ) ) {
451 $active = is_plugin_active( $report['file_path'] ) ? __( 'Yes', 'plugin-report' ) : __( 'No', 'plugin-report' );
452 $css_class = is_plugin_active( $report['file_path'] ) ? self::CSS_CLASS_LOW : self::CSS_CLASS_HIGH;
453 }
454 $html .= '<td class="' . $css_class . '">' . $active . '</td>';
455 }
456
457 // Installed / available version.
458 if ( isset( $report['repo_info'] ) ) {
459 $css_class = $this->get_version_risk_classname( $report['local_info']['Version'], $report['repo_info']->version );
460 $html .= '<td class="' . $css_class . '">';
461 $html .= $report['local_info']['Version'];
462 if ( $report['local_info']['Version'] !== $report['repo_info']->version ) {
463 // Any platform upgrades needed?
464 $needs_php_upgrade = isset( $report['repo_info']->requires_php ) ? version_compare( phpversion(), $report['repo_info']->requires_php, '<' ) : false;
465 $needs_wp_upgrade = isset( $report['repo_info']->requires ) ? version_compare( $wp_version, $report['repo_info']->requires, '<' ) : false;
466 // Create the additional message.
467 if ( $needs_wp_upgrade && $needs_php_upgrade ) {
468 /* translators: %1$s: Plugin version number, %2$s: WP version number, %3$s: PHP version number */
469 $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>';
470 } elseif ( $needs_wp_upgrade ) {
471 /* translators: %1$s: Plugin version number, %2$s: WP version number. */
472 $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>';
473 } elseif ( $needs_php_upgrade ) {
474 /* translators: %1$s: Plugin version number, %2$s: PHP version number. */
475 $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>';
476 } else {
477 /* translators: %s: Plugin version number. */
478 $html .= ' <span class="pr-additional-info">' . sprintf( esc_html__( '(%s available)', 'plugin-report' ), $report['repo_info']->version ) . '</span>';
479 }
480 }
481 $html .= '</td>';
482 } else {
483 $html .= '<td>' . $report['local_info']['Version'] . '</td>';
484 }
485
486 // Auto-update.
487 if ( version_compare( $wp_version, '5.5', '<' ) ) {
488 $html .= '<td>' . __( 'Requires WordPress 5.5 or higher', 'plugin-report' ) . '</td>';
489 } else {
490 if ( isset( $report['auto-update'] ) && $report['auto-update'] ) {
491 $html .= '<td class="' . self::CSS_CLASS_LOW . '">' . __( 'Enabled', 'plugin-report' ) . '</td>';
492 } else {
493 $html .= '<td>' . __( 'Not enabled', 'plugin-report' ) . '</td>';
494 }
495 }
496
497 // Last updates.
498 if ( isset( $report['repo_info'] ) && isset( $report['repo_info']->last_updated ) ) {
499 $time_update = new DateTime( $report['repo_info']->last_updated );
500 $time_diff = human_time_diff( $time_update->getTimestamp(), current_time( 'timestamp' ) );
501 $css_class = $this->get_timediff_risk_classname( current_time( 'timestamp' ) - $time_update->getTimestamp() );
502 $html .= '<td class="' . $css_class . '" data-sort="' . $time_update->getTimestamp() . '">' . $time_diff . '</td>';
503 } else {
504 $html .= $this->render_error_cell();
505 }
506
507 // Tested up to.
508 if ( isset( $report['repo_info'] ) && isset( $report['repo_info']->tested ) ) {
509 $css_class = $this->get_version_risk_classname( $report['repo_info']->tested, $wp_latest, true );
510 $html .= '<td class="' . $css_class . '">' . $report['repo_info']->tested . '</td>';
511 } else {
512 $html .= $this->render_error_cell();
513 }
514
515 // Overall user rating.
516 if ( isset( $report['repo_info'] ) && isset( $report['repo_info']->num_ratings ) && isset( $report['repo_info']->rating ) ) {
517 $css_class = ( intval( $report['repo_info']->num_ratings ) > 0 ) ? $this->get_percentage_risk_classname( intval( $report['repo_info']->rating ) ) : '';
518 $value_text = ( ( intval( $report['repo_info']->num_ratings ) > 0 ) ? $report['repo_info']->rating . '%' : esc_html__( 'No data available', 'plugin-report' ) );
519 $html .= '<td class="' . $css_class . '">' . $value_text . '</td>';
520 } else {
521 $html .= $this->render_error_cell();
522 }
523
524 // Close the new table row.
525 $html .= '</tr>';
526 }
527 return $html;
528 }
529
530
531 /**
532 * Format an error message as a table row, so we can return it to javascript.
533 *
534 * @param string $message Message to be shown.
535 */
536 private function render_error_row( $message ) {
537 return '<tr class="pluginreport-row-error"><td colspan="' . self::COLS_PER_ROW . '">' . $message . '</td></tr>';
538 }
539
540
541 /**
542 * Format an error message as a table cell, so we can return it to javascript.
543 *
544 * @param string $message Message to be shown.
545 */
546 private function render_error_cell( $message = null ) {
547 if ( ! $message ) {
548 $message = esc_html__( 'No data available', 'plugin-report' );
549 }
550 return '<td class="pluginreport-cell-error">' . $message . '</td>';
551 }
552
553
554 /**
555 * Return the version string with all elements beyond the second removed ("5.5.1" -> "5.5").
556 *
557 * @param string $version_string Complete version number.
558 */
559 private function get_major_version( $version_string ) {
560 $parts = explode( '.', $version_string );
561 array_splice( $parts, 2 );
562 return implode( '.', $parts );
563 }
564
565
566 /**
567 * Figure out what CSS class to use based on current and optimal version numbers.
568 *
569 * @param string $available Available version.
570 * @param string $optimal Optimal version.
571 * @param bool $major_only True to compare only major versions, false otherwise.
572 */
573 private function get_version_risk_classname( $available, $optimal, $major_only = false ) {
574 // Use only the first two elements of the version number if $major_only is set to true.
575 // This is used for WP version numbers, where point releases are not considered a risk.
576 if ( $major_only ) {
577 $available = $this->get_major_version( $available );
578 $optimal = $this->get_major_version( $optimal );
579 }
580 // If the version is equal or higher, indicate low risk.
581 if ( version_compare( $available, $optimal, '>=' ) ) {
582 return self::CSS_CLASS_LOW;
583 }
584 // Else, indicate high risk.
585 return self::CSS_CLASS_HIGH;
586 }
587
588
589 /**
590 * Assess the risk associated with low ratings or poor compatibility feedback, return corresponding CSS class.
591 *
592 * @param int $perc Rating percentage.
593 */
594 private function get_percentage_risk_classname( $perc ) {
595 if ( $perc < 70 ) {
596 return self::CSS_CLASS_HIGH;
597 }
598 if ( $perc < 90 ) {
599 return self::CSS_CLASS_MED;
600 }
601 return self::CSS_CLASS_LOW;
602 }
603
604
605 /**
606 * Assess the risk associated with low ratings or poor compatibility feedback, return corresponding CSS class.
607 *
608 * @param string $time_diff Time difference.
609 */
610 private function get_timediff_risk_classname( $time_diff ) {
611 $days = $time_diff / ( DAY_IN_SECONDS );
612 if ( $days > 365 ) {
613 return self::CSS_CLASS_HIGH;
614 }
615 if ( $days > 90 ) {
616 return self::CSS_CLASS_MED;
617 }
618 return self::CSS_CLASS_LOW;
619 }
620
621
622 /**
623 * Get the latest available WordPress version using WP core functions
624 * This way, we don't need to do any API calls. WP check this periodically anyway.
625 */
626 private function check_core_updates() {
627 global $wp_version;
628 $update = get_preferred_from_update_core();
629 // Bail out of no valid response, or false.
630 if ( ! $update || false === $update ) {
631 return $wp_version;
632 }
633 // If latest, return current version number.
634 if ( 'latest' === $update->response ) {
635 return $wp_version;
636 }
637 // Return the preferred update's version number.
638 return $update->version;
639 }
640
641
642 /**
643 * Gather statistics about a plugin's activation on a multisite install.
644 *
645 * @param string $path Plugin path.
646 */
647 private function get_multisite_activation( $path ) {
648 // Create an array to contain the return values.
649 $activation_status = array(
650 'network' => false,
651 'active' => 0,
652 'sites' => 1,
653 );
654 // Check if the plugin is network activated.
655 $network_plugins = get_site_option( 'active_sitewide_plugins', null );
656 if ( array_key_exists( $path, $network_plugins ) ) {
657 $activation_status['network'] = true;
658 } else {
659 // Get a list of all sites in the multisite install.
660 $args = array(
661 'number' => 9999,
662 'fields' => 'ids',
663 );
664 $sites = get_sites( $args );
665 // Add the total number of sites to the return array.
666 $activation_status['sites'] = count( $sites );
667 // Loop through the sites to find where the plugin is active.
668 foreach ( $sites as $site_id ) {
669 $plugins = get_blog_option( $site_id, 'active_plugins', null );
670 if ( $plugins ) {
671 foreach ( $plugins as $plugin_path ) {
672 if ( $plugin_path === $path ) {
673 $activation_status['active']++;
674 }
675 }
676 }
677 }
678 }
679 // Return the data we gathered.
680 return $activation_status;
681 }
682
683
684 /**
685 * Create a cache key that is unique to the provided plugin slug.
686 *
687 * @param string $slug Plugin slug.
688 */
689 private function create_cache_key( $slug ) {
690 // Create a hash for the plugin slug.
691 $slug_hash = hash( 'sha256', $slug );
692 // Prefix and limit the string to 40 characters to avoid issues with long keys.
693 $cache_key = 'rtpr_' . substr( $slug_hash, 0, 35 );
694 // Return the key.
695 return $cache_key;
696 }
697
698
699 /**
700 * Clear all cached plugin info
701 */
702 private function clear_cache() {
703 // Request a list of all plugins.
704 $plugins = get_plugins();
705 // Loop through the plugins array, and delete cache items.
706 foreach ( $plugins as $key => $plugin ) {
707 $slug = $this->get_plugin_slug( $key );
708 $this->clear_cache_item( $slug );
709 }
710 }
711
712
713 /**
714 * Remove the cache item for a single plugin.
715 *
716 * @param string $slug Plugin slug.
717 */
718 private function clear_cache_item( $slug ) {
719 if ( isset( $slug ) ) {
720 $cache_key = $this->create_cache_key( $slug );
721 delete_site_transient( $cache_key );
722 }
723 }
724
725
726 /**
727 * Selectively delete cache for plugins that have been updated.
728 */
729 public function upgrade_delete_cache_items( $upgrader, $data ) {
730 // Check if plugins have been upgraded by WP.
731 if ( isset( $data ) && isset( $data['plugins'] ) && is_array( $data['plugins'] ) ) {
732 // Loop through the plugins, and delete the associated cache items.
733 foreach ( $data['plugins'] as $key => $value ) {
734 $slug = $this->get_plugin_slug( $value );
735 $this->clear_cache_item( $slug );
736 }
737 }
738 }
739
740 }
741
742 // Instantiate the class.
743 $plugin_report_instance = new RT_Plugin_Report();
744 $plugin_report_instance->init();
745
746 }
747