├── README.md ├── classes ├── UpdateClient.class.php └── index.php ├── codepotent-php-error-log-viewer.php ├── images ├── banner-1544x500.png ├── banner-772x250.png ├── icon-128.png ├── icon-256.png ├── index.php ├── screenshot-1.png ├── screenshot-2.png └── screenshot-3.png ├── includes ├── constants.php ├── functions.php └── index.php ├── index.php ├── scripts ├── global.js └── index.php └── styles ├── global.css └── index.php /README.md: -------------------------------------------------------------------------------- 1 | The PHP Error Log Viewer plugin for ClassicPress brings your error log straight into your dashboard. Color-coding helps you to quickly scan even the longest of error logs. Or, just filter out the errors you don't want to see. No more wall-of-text error messages – this plugin turns your PHP error log into an incredibly useful display. 2 | 3 |  4 | 5 | ## Fast, Lightweight, and User-Friendly 6 | 7 | There are lots of debugging plugins out there – _debugging suites_, really. This plugin isn't intended to become one of them. The PHP Error Log Viewer plugin handles a very specific task and that is to display the PHP error log in a user-friendly manner than can be easily filtered, styled, sorted, preserved, or purged. 8 | 9 | ## More Debugging, Less Clicking 10 | 11 | If you have grown tired of flipping back and forth between screens/browsers/apps/whatever to check and recheck your PHP error log as you're writing code, this will be incredibly handy for you. There's a link to the error log within reach at all times and it doesn't require any special configuration. 12 | 13 | 14 | ### Viewing the Error Log 15 | Click the `PHP Errors` menu item in your admin bar. Hover the menu item momentarily and it will reveal your current PHP version. Alternatively, you can access the error log by navigating to `Dashboard > Tools > PHP Error Log`. 16 | 17 |  18 | 19 | ### Filtering the Error Log 20 | The checkboxes across the top of the display allow you to show and hide each of the various types of errors: `Deprecated`, `Notice`, `Warning`, `Error`, and `Other`. There are also checkboxes to show and hide the time/date, stack traces, and to sort the error log in reverse. Tick your preferred boxes and click the `Apply Filters` button to update the display. 21 | 22 |  23 | 24 | ### Refreshing the Error Log 25 | When viewing the error log, you will find a button to `Refresh Error Log` at the right side of the page. Clicking this button has the same effect as clicking your browser's refresh button. The error log will be re-read and displayed fresh. 26 | 27 |  28 | 29 | ### Purging the Error Log 30 | When viewing the error log, you will find a button to `Purge Error Log` at the right side of the page. Clicking this button will purge all messages from the error log. A confirmation dialog prevents accidental deletion. If your error log is not writable by the PHP process, you will not see this button. 31 | 32 |  33 | 34 | ### Purging the Error Log via AJAX 35 | In the admin bar, you will find a link `PHP Errors` which, when hovered, will expose a link to `Purge Error Log`. Clicking this button will purge all messages from the error log without redirecting you away from the current page. A confirmation dialog prevents accidental deletion. If your error log is not writable by the PHP process, you will not see this link. 36 | 37 |  38 | 39 | ### Manually Triggering Errors 40 | As of version 2.2.0, there is a function that allows you to manually trigger user-level notices, warnings, or errors and have them neatly displayed in the error log. Here is an example of creating your own wrapper function for added convenience. 41 | 42 | ``` 43 | /** 44 | * Creating your own error logging wrapper function 45 | * 46 | * This example shows how you might integrate the logging function into your own 47 | * utility plugin. 48 | * 49 | * @param mixed $data Pass in a string, integer, array, object, etc. 50 | * @param str $level Must be notice, warning, or error. 51 | * @param int $file Use __FILE__ constant to include filename. 52 | * @param bool $line Use __LINE__ constant to include line number. 53 | */ 54 | function log_data($data, $level='notice', $file=false, $line=false) { 55 | 56 | // If error log plugin is active and the needed function exists... 57 | if (function_exists('codepotent_php_error_log_viewer_log')) { 58 | return codepotent_php_error_log_viewer_log($data, $level, $file, $line); 59 | } 60 | 61 | // Or, if error log plugin is inactive, you can include just the needed function... 62 | if (file_exists($file = plugin_dir_path(__DIR__).'codepotent-php-error-log-viewer/includes/functions.php')) { 63 | require_once($file); 64 | return codepotent_php_error_log_viewer_log($data, $level, $file, $line); 65 | } 66 | 67 | // If the error log plugin just doesn't exist, there's a fallback. 68 | trigger_error(print_r($data, true), E_USER_WARNING); 69 | 70 | } 71 | 72 | // Elsewhere, send data to the log like this: 73 | $data = 'whatever type of data'; 74 | log_data($data, 'notice', __FILE__, __LINE__); 75 | ``` 76 | --- 77 | 78 | ### Display Options 79 | The checkboxes at the top of the error log display allow you to choose which types of error messages you want to see. Check any of the boxes and click the `Apply Filter` button to update the display. 80 | * **Date/Time** 81 | Check this box to show dates, times, and other meta data. 82 | * **Notice** 83 | Check this box to show non-critical PHP notices. 84 | * **Warning** 85 | Check this box to show non-critical PHP warnings. 86 | * **Error** 87 | Check this box to show critical PHP errors. 88 | * **Other** 89 | Check this box to show any other errors that didn't meet the above criteria. 90 | * **Show Stack Traces** 91 | Check this box to show stack traces for critical errors. Note that not all critical errors will generate a stack trace. 92 | * **Reverse Sort** 93 | Check this box to display the error log with latest errors at the top. 94 | 95 | --- 96 | 97 | ### Primary Alert Bubble This filter allows you to hide or redesign the primary (red) alert bubble in the admin bar. This filter accepts a single argument, the markup of the primary alert bubble. 98 | 99 |
function yourprefix_hide_primary_alert($alert) { 100 | return ''; 101 | } 102 | add_filter('codepotent_php_errror_log_viewer_primary_alert', 'yourprefix_hide_primary_alert'); 103 |104 | 105 | --- ### Secondary Alert Bubble This filter will allow you to hide or redesign the secondary (gray) alert bubble in the admin bar. This filter accepts a single argument, the markup of the secondary alert bubble. 106 | 107 |
function yourprefix_hide_secondary_alert($alert) { 108 | return ''; 109 | } 110 | add_filter('codepotent_php_errror_log_viewer_primary_alert', 'yourprefix_hide_secondary_alert'); 111 |112 | 113 | --- 114 | 115 | ### Add Content Before Legend 116 | In cases where you need to insert some contextual information, either of the following filters can be used to place the content before or after the legend. These filters receive an empty string as an argument. 117 | 118 |
function yourprefix_before_error_log_legend($markup) { 119 | $markup = '124 | 125 | --- 126 | ### Add Content After Legend 127 | Identical to the filter above, except this filter places your contextual content below the legend. 128 | 129 |This content appears before the legend.
'; 120 | return $markup; 121 | } 122 | add_filter('codepotent_php_errror_log_viewer_before_legend', 'yourprefix_before_error_log_legend'); 123 |
function yourprefix_after_error_log_legend($markup) { 130 | $markup = '135 | 136 | --- 137 | 138 | ### Using Custom Error Colors 139 | 140 | To override the color-coding for the error messages and legend, copy the following styles into your theme'sThis content appears after the legend.
'; 131 | return $markup; 132 | } 133 | add_filter('codepotent_php_errror_log_viewer_after_legend', 'yourprefix_after_error_log_legend'); 134 |
style.css
file and make your changes there.
141 |
142 | 143 | /* Deprecated code. */ 144 | #codepotent-php-error-log-viewer .php-deprecated, 145 | .codepotent-php-error-log-viewer-legend-box.item-php-deprecated { 146 | border-left:10px solid #847545; 147 | } 148 | /* Notices. */ 149 | #codepotent-php-error-log-viewer .php-notice, 150 | .codepotent-php-error-log-viewer-legend-box.item-php-notice { 151 | border-left:10px solid #ccc; 152 | } 153 | /* Warnings. */ 154 | #codepotent-php-error-log-viewer .php-warning, 155 | .codepotent-php-error-log-viewer-legend-box.item-php-warning { 156 | border-left:10px solid #ffee58; 157 | } 158 | /* Errors. */ 159 | #codepotent-php-error-log-viewer .php-error, 160 | .codepotent-php-error-log-viewer-legend-box.item-php-error { 161 | border-left:10px solid #e53935; 162 | } 163 | /* Stack traces. */ 164 | #codepotent-php-error-log-viewer .php-stack-trace-title, 165 | #codepotent-php-error-log-viewer .php-stack-trace-step, 166 | #codepotent-php-error-log-viewer .php-stack-trace-origin, 167 | .codepotent-php-error-log-viewer-legend-box.item-php-stack-trace-title { 168 | border-left:10px solid #ef9a9a; 169 | } 170 | /* Any other messages. */ 171 | #codepotent-php-error-log-viewer .php-other, 172 | .codepotent-php-error-log-viewer-legend-box.item-php-other { 173 | border-left:10px solid #00bcd4; 174 | } 175 | 176 |-------------------------------------------------------------------------------- /classes/UpdateClient.class.php: -------------------------------------------------------------------------------- 1 | config = [ 87 | // The URL where your Update Manager plugin is installed. 88 | 'server' => UPDATE_SERVER, 89 | // Leave as-is; may add support for theme updates later. 90 | 'type' => UPDATE_TYPE, 91 | // Plugin identifier; ie, plugin-folder/plugin-file.php. 92 | 'id' => $this->get_identifier(), 93 | // Leave as-is. 94 | 'api' => '2.0.0', 95 | // Leave as-is – tutorial can be created with enough interest. 96 | 'post' => [], 97 | ]; 98 | 99 | // Find and store the latest CP version during update process. 100 | $this->cp_latest_version = get_option('cp_latest_version', ''); 101 | 102 | // Hook the update client into the system. 103 | $this->init(); 104 | 105 | } 106 | 107 | /** 108 | * Get instance of object. 109 | * 110 | * Returns the current instance of the object. Or, returns a new instance of 111 | * the object if it hasn't yet been instantiated. 112 | * 113 | * @author John Alarcon 114 | * 115 | * @since 1.0.0 116 | * 117 | * @return object Current instance of the object. 118 | */ 119 | public static function get_instance() { 120 | 121 | // Check for existing instance or get a new one. 122 | if (self::$instance === null) { 123 | self::$instance = new self; 124 | } 125 | 126 | // Return the object. 127 | return self::$instance; 128 | 129 | } 130 | 131 | /** 132 | * Initialize the update manager client. 133 | * 134 | * Hook in actions and filters. 135 | * 136 | * @author John Alarcon 137 | * 138 | * @since 1.0.0 139 | */ 140 | private function init() { 141 | 142 | // Print footer scripts; see comments on the method. 143 | add_action('admin_print_footer_scripts', [$this, 'print_admin_scripts']); 144 | 145 | // Filter the admin row links. 146 | add_filter($this->config['type'].'_row_meta', [$this, 'filter_component_row_meta'], 10, 2); 147 | 148 | // Filter update data into the transient before saving. 149 | add_filter('pre_set_site_transient_update_'.$this->config['type'].'s', [$this, 'filter_component_update_transient']); 150 | 151 | // Filter install API results. 152 | add_filter($this->config['type'].'s_api_result', [$this, 'filter_components_api_result'], 10, 3); 153 | 154 | // Filter after-install process. 155 | add_filter('upgrader_post_install', [$this, 'filter_upgrader_post_install'], 11, 3); 156 | 157 | } 158 | 159 | /** 160 | * Print admin scripts. 161 | * 162 | * A jQuery one-liner is required to swap version numbers dynamically in the 163 | * modal windows. Also, a few styles are required to removed the rating area 164 | * that is autolinked to the WordPress repository. 165 | * 166 | * Note that scripts and styles should be enqueued with the proper hooks and 167 | * not printed directly (as is done here) unless there is a valid reason for 168 | * doing so. In this case, the valid reason is simply that this update class 169 | * is intended to be a single file in your plugin or theme; if you enqueue a 170 | * file, it must be an actual file – this would add needless complication to 171 | * implementing the class. 172 | * 173 | * @author John Alarcon 174 | * 175 | * @since 1.0.0 176 | */ 177 | public function print_admin_scripts() { 178 | 179 | // Grab the current screen. 180 | $screen = get_current_screen(); 181 | 182 | // Only need this JS/CSS on the plugin admin page and updates page. 183 | if ($screen->base === 'plugins' || $screen->base === 'plugin-install') { 184 | // This will make the jQuery below work with various languages. 185 | $text1 = esc_html__('Compatible up to:'); 186 | $text2 = esc_html__('Reviews'); 187 | $text3 = esc_html__('Read all reviews'); 188 | // Swap "Compatible up to: 4.9.99" with "Compatible up to: 1.1.1". 189 | echo ''."\n"; 190 | // Styles for the modal window. 191 | echo ''."\n"; 212 | } 213 | 214 | } 215 | 216 | /** 217 | * Filter the update transient. 218 | * 219 | * @author John Alarcon 220 | * 221 | * @since 2.0.0 222 | * 223 | * @param object $value 224 | * @return object $value 225 | */ 226 | public function filter_component_update_transient($value) { 227 | 228 | // Is there a response? 229 | if (isset($value->response)) { 230 | 231 | // Ensure the latest ClassicPress version number is available. 232 | $this->get_latest_version_number(); 233 | 234 | // Get the installed components. 235 | $components = $this->get_component_data('query_'.$this->config['type'].'s'); 236 | 237 | // Iterate over installed components. 238 | foreach($components as $component=>$data) { 239 | 240 | // Is there a new version? 241 | if (isset($data['id'], $data['new_version'], $data['package'])) { 242 | 243 | // Add update data to response. 244 | if ($this->config['type'] === 'plugin') { 245 | 246 | // Plugin images. 247 | $icons = $banners = $screenshots = []; 248 | if (!empty($icons = $this->get_plugin_images('icon', dirname($component)))) { 249 | $data['icons'] = $icons; 250 | } 251 | if (!empty($banners = $this->get_plugin_images('banner', dirname($component)))) { 252 | $data['banners'] = $banners; 253 | } 254 | if (!empty($screenshots = $this->get_plugin_images('screenshot', dirname($component)))) { 255 | $data['screenshots'] = $screenshots; 256 | } 257 | // Cast as object. 258 | $value->response[$component] = (object)$data; 259 | 260 | } else if ($this->config['type'] === 'theme') { 261 | 262 | // Cast as array. 263 | $value->response[$component] = (array)$data; 264 | 265 | } 266 | 267 | } else { 268 | 269 | // If no new version, no update. Unset the entry. 270 | unset($value->response[$component]); 271 | 272 | } // if/else 273 | 274 | } // foreach $components 275 | 276 | } // isset($value->response) 277 | 278 | // Return the updated transient value. 279 | return $value; 280 | 281 | } 282 | 283 | /** 284 | * Filter the update transient. 285 | * 286 | * @author John Alarcon 287 | * 288 | * @since 1.0.0 289 | * 290 | * @deprecated 2.0.0 Use filter_component_update_transient() method instead. 291 | * 292 | * @param object $value 293 | * @return object $value 294 | */ 295 | public function filter_plugin_update_transient($value) { 296 | return $this->filter_component_update_transient($value); 297 | } 298 | 299 | /** 300 | * Filter the API result. 301 | * 302 | * @author John Alarcon 303 | * 304 | * @since 2.0.0 305 | * 306 | * @param object $res 307 | * @param string $action 308 | * @param object $args 309 | * @return object $res 310 | */ 311 | public function filter_components_api_result($res, $action, $args) { 312 | 313 | // If needed args are missing, just return the result. 314 | if (empty($args->slug) || $action !== $this->config['type'].'_information') { 315 | return $res; 316 | } 317 | 318 | // Create an array of the plugin or theme slug and identifier. 319 | $list_components = [ 320 | dirname($this->config['id']) => $this->config['id'], 321 | ]; 322 | 323 | // Check if component exists 324 | if (!array_key_exists($args->slug, $list_components)) { 325 | return $res; 326 | } 327 | 328 | // Get the component's information. 329 | $info = $this->get_component_data($action, $list_components[$args->slug]); 330 | 331 | // If the response has all the right properties, cast $info to object. 332 | if (isset($info['name'], $info['slug'], $info['external'], $info['sections'])) { 333 | $res = (object)$info; 334 | } 335 | 336 | // Return response. 337 | return $res; 338 | 339 | } 340 | 341 | /** 342 | * Filter plugins API result. 343 | * 344 | * @author John Alarcon 345 | * 346 | * @since 1.0.0 347 | * 348 | * @deprecated 2.0.0 Use filter_components_api_result() method instead. 349 | * 350 | * @param object $res 351 | * @param string $action 352 | * @param object $args 353 | * @return object $res 354 | */ 355 | public function filter_plugins_api_result($res, $action, $args) { 356 | return $this->filter_components_api_result($res, $action, $args); 357 | } 358 | 359 | /** 360 | * Filter admin row meta. 361 | * 362 | * A method to add a "View Details" link to the admin row item. 363 | * 364 | * @author John Alarcon 365 | * 366 | * @since 2.0.0 367 | * 368 | * @param array $component_meta Array of metadata (links, typically) 369 | * @param string $component_file Ex: plugin-folder/plugin-file.php 370 | * @return array $component_meta with an added link. 371 | */ 372 | public function filter_component_row_meta($component_meta, $component_file) { 373 | 374 | // Add the link to the plugin's or theme's row, if not already existing. 375 | if ($this->identifier === $component_file) { 376 | $anchors_string = implode('', $component_meta); 377 | $anchor_text = esc_html__('View details'); 378 | if (!preg_match('|(\)|', $anchors_string)) { 379 | $component_meta[] = ''.$anchor_text.''; 380 | } 381 | } 382 | 383 | // Return the maybe amended links. 384 | return $component_meta; 385 | 386 | } 387 | 388 | /** 389 | * Filter plugin row meta. 390 | * 391 | * A method to add a "View Details" link to the plugin's admin row item. 392 | * 393 | * @author John Alarcon 394 | * 395 | * @since 1.0.0 396 | * 397 | * @deprecated 2.0.0 Use filter_component_row_meta() method instead. 398 | * 399 | * @param array $plugin_meta Array of metadata (links, typically) 400 | * @param string $plugin_file Ex: plugin-folder/plugin-file.php 401 | * @return array $plugin_meta with an added link. 402 | */ 403 | public function filter_plugin_row_meta($plugin_meta, $plugin_file) { 404 | return $this->filter_component_row_meta($plugin_meta, $plugin_file); 405 | } 406 | 407 | /** 408 | * Filter post-installer. 409 | * 410 | * @author John Alarcon 411 | * 412 | * @since 1.0.0 413 | * 414 | * @param object $response 415 | * @param array $hook_extra 416 | * @param array $result 417 | * @return object 418 | */ 419 | public function filter_upgrader_post_install($response, $hook_extra, $result) { 420 | 421 | // Not dealing with an install? Bail. 422 | if (!isset($hook_extra[$this->config['type']])) { 423 | return $response; 424 | } 425 | 426 | // Bring variables into scope. 427 | global $wp_filesystem, $hook_suffix; 428 | 429 | // Destination for new component. 430 | $destination = trailingslashit($result['local_destination']).dirname($hook_extra[$this->config['type']]); 431 | 432 | // Move the component to the correct location. 433 | $wp_filesystem->move($result['destination'], $destination); 434 | 435 | // Match'em up. 436 | $result['destination'] = $destination; 437 | 438 | // Set destination name. 439 | $result['destination_name'] = dirname($hook_extra[$this->config['type']]); 440 | 441 | // Updating a plugin or theme? 442 | if ($hook_suffix === 'update') { 443 | // Got both of the needed arguments? 444 | if (isset($_GET['action'], $_GET[$this->config['type']])) { 445 | // First argument is good? 446 | if ($_GET['action'] === 'upgrade-'.$this->config['type']) { 447 | // Next argument is good? 448 | if ($_GET[$this->config['type']] === $hook_extra[$this->config['type']]) { 449 | // Activate the component. 450 | $function = ($this->config['type'] === 'plugin') ? 'activate_plugin' : 'activate_theme'; 451 | $function($hook_extra[$this->config['type']]); 452 | } 453 | } 454 | } 455 | } 456 | 457 | // Return the response unaltered. 458 | return $response; 459 | 460 | } 461 | 462 | /** 463 | * Get component identifier. 464 | * 465 | * A plugin identifier (ie, plugin-folder/plugin-file.php) may possibly have 466 | * different locations in different implementations. This method is reliable 467 | * in determining the identifier, regardless of where the file may exist. In 468 | * the case of themes, the identifier is simply a directory name; the method 469 | * works for this, as well. 470 | * 471 | * @author John Alarcon 472 | * 473 | * @since 2.0.0 474 | * 475 | * @return string Component identifier; ie, plugin-folder/plugin-file.php or 476 | * ie, some-theme-directory 477 | */ 478 | private function get_identifier() { 479 | 480 | $identifier = ''; 481 | 482 | if (UPDATE_TYPE === 'theme') { 483 | 484 | $path_parts = explode('/', str_replace('\\', '/', __FILE__)); 485 | foreach ($path_parts as $n=>$part) { 486 | if ($part === 'themes') { 487 | $this->identifier = $identifier = $path_parts[$n+1]; 488 | break; 489 | } 490 | } 491 | 492 | } else if (UPDATE_TYPE === 'plugin') { 493 | 494 | // Gain access the get_plugins() function. 495 | include_once(ABSPATH.'/wp-admin/includes/plugin.php'); 496 | 497 | // Get path to plugin dir and this file; make consistent the slashes. 498 | $dir = explode('/', str_replace('\\', '/', WP_PLUGIN_DIR)); 499 | $file = explode('/', str_replace('\\', '/', __FILE__)); 500 | 501 | // Strip plugin dir parts, leaving this plugin's directory at $diff[0]. 502 | $diff = array_diff($file, $dir); 503 | 504 | // This plugin's directory name. 505 | $this->server_slug = $dir_name = array_shift($diff); 506 | 507 | // Initialization. 508 | $identifier = ''; 509 | 510 | // Find the plugin id that matches the directory name. 511 | foreach (array_keys(get_plugins()) as $id) { 512 | if (strpos($id, $dir_name.'/') === 0) { 513 | $this->identifier = $identifier = $id; 514 | break; 515 | } 516 | } 517 | } 518 | 519 | // Return the identifier. 520 | return $identifier; 521 | 522 | } 523 | 524 | /** 525 | * Get plugin identifier. 526 | * 527 | * The plugin identifier (ie, plugin-folder/plugin-file.php) will differ for 528 | * different implementations. This method is a reliable way to determine the 529 | * directory name and primary PHP file of the plugin, without any assumption 530 | * of where this file may exist. 531 | * 532 | * @author John Alarcon 533 | * 534 | * @since 1.0.0 535 | * 536 | * @deprecated 2.0.0 Replaced with get_identifier() method. 537 | * 538 | * @return string Plugin identifier; ie, plugin-folder/plugin-file.php 539 | */ 540 | private function get_plugin_identifier() { 541 | return $this->get_identifier(); 542 | } 543 | 544 | /** 545 | * Get component data. 546 | * 547 | * @author John Alarcon 548 | * 549 | * @since 2.0.0 550 | * 551 | * @param string $action May be any of the following: 552 | * plugin_information 553 | * theme_information 554 | * query_plugins 555 | * query_themes 556 | * @param string $component Will be 'plugin' or 'theme'. 557 | * @return array|array|mixed Data for the plugin or theme. 558 | */ 559 | private function get_component_data($action, $component='') { 560 | 561 | // If component data exists, no need to requery; return that data. 562 | if (!empty($this->component_data)) { 563 | return $this->component_data; 564 | } 565 | 566 | // Localize the platform version. 567 | global $cp_version; 568 | 569 | // Initialize the data to be posted. 570 | $body = $this->config['post']; 571 | 572 | if ($action === 'plugin_information') { 573 | 574 | // If querying a single plugin, assign it to the post body. 575 | $body[$this->config['type']] = $component; 576 | 577 | } else if ($action === 'theme_information') { 578 | 579 | // If querying a single theme, assign it to the post body. 580 | $body[$this->config['type']] = $component; 581 | 582 | } else if ($action === 'query_plugins') { 583 | 584 | // If querying for all plugins, assign them to the post body. 585 | $body['plugins'] = get_plugins(); 586 | 587 | } else if ($action === 'query_themes') { 588 | 589 | // If querying for all themes, preprocess and assign to post body. 590 | $themes = []; 591 | $get_themes = wp_get_themes(); 592 | foreach ($get_themes as $theme) { 593 | $stylesheet = $theme->get_stylesheet(); 594 | $themes[$stylesheet] = [ 595 | 'Name' => $theme->get('Name'), 596 | 'ThemeURI' => $theme->get('ThemeURI'), 597 | 'Description' => $theme->get('Description'), 598 | 'Author' => $theme->get('Author'), 599 | 'AuthorURI' => $theme->get('AuthorURI'), 600 | 'Version' => $theme->get('Version'), 601 | 'Template' => $theme->get('Template'), 602 | 'Status' => $theme->get('Status'), 603 | 'Tags' => $theme->get('Tags'), 604 | 'TextDomain' => $theme->get('TextDomain'), 605 | 'DomainPath' => $theme->get('DomainPath'), 606 | ]; 607 | } 608 | $body['themes'] = $themes; 609 | 610 | } else { 611 | 612 | return []; 613 | 614 | } 615 | 616 | // Site URL; allows for particular URLs to test updates before pushing. 617 | $body['site_url'] = site_url(); 618 | 619 | // Images, if any. 620 | if ($this->config['type'] === 'plugin') { 621 | $body['icon_urls'] = $this->get_plugin_images('icon', dirname($component)); 622 | $body['banner_urls'] = $this->get_plugin_images('banner', dirname($component)); 623 | $body['screenshot_urls'] = $this->get_plugin_images('screenshot', dirname($component)); 624 | } 625 | 626 | // Assemble args to post back to the Update Manager plugin. 627 | $options = [ 628 | 'user-agent' => 'ClassicPress/'.$cp_version.'; '.get_bloginfo('url'), 629 | 'body' => $body, 630 | 'timeout' => apply_filters('codepotent_update_manager_timeout', 5), 631 | ]; 632 | 633 | // Args to append to the endpoint URL. 634 | $url_args = [ 635 | 'update' => $action, 636 | $this->config['type'] => $this->config['id'], 637 | ]; 638 | 639 | // Setup both HTTP and HTTPS endpoint URLs. 640 | $server = set_url_scheme($this->config['server'], 'http'); 641 | $url = $http_url = add_query_arg($url_args, $server); 642 | if (wp_http_supports(['ssl'])) { 643 | $url = set_url_scheme($url, 'https'); 644 | } 645 | 646 | // Try posting the data via HTTPS as a first course. 647 | $raw_response = wp_remote_post(esc_url_raw($url), $options); 648 | 649 | // If remote post failed, try again over HTTP as a fallback. 650 | if (is_wp_error($raw_response)) { 651 | $raw_response = wp_remote_post(esc_url_raw($http_url), $options); 652 | } 653 | 654 | // Still an error? Hey, you tried. Bail. 655 | if (is_wp_error($raw_response) || 200 != wp_remote_retrieve_response_code($raw_response)) { 656 | return []; 657 | } 658 | 659 | // Get the response body; decode it as an array. 660 | $data = json_decode(trim(wp_remote_retrieve_body($raw_response)), true); 661 | 662 | // Set retrieved data to the object for reuse elsewhere. 663 | $this->component_data = is_array($data) ? $data : []; 664 | 665 | // Return the reponse body. 666 | return $this->component_data; 667 | 668 | } 669 | 670 | /** 671 | * Get plugin data. 672 | * 673 | * @author John Alarcon 674 | * 675 | * @since 1.0.0 676 | * 677 | * @deprecated 2.0.0 Replaced with get_component_data() method. 678 | * 679 | * @param string $action 680 | * @param string $plugin 681 | * @return array|array|mixed 682 | */ 683 | private function get_plugin_data($action, $plugin='') { 684 | return $this->get_component_data($action, $plugin); 685 | } 686 | 687 | /** 688 | * Get plugin images. 689 | * 690 | * This method returns URLs to the plugin's icon and banner images which are 691 | * used throughout the update process and screens. 692 | * 693 | * @author John Alarcon 694 | * 695 | * @since 1.0.0 696 | * 697 | * @param string $type Either 'icon' or 'banner'. 698 | * @param string $plugin The name (ie, folder-name) of a plugin. 699 | * @return array Array of image URLs or empty array. 700 | */ 701 | public function get_plugin_images($type, $plugin) { 702 | 703 | // Initialize. 704 | $images = []; 705 | 706 | // Need argument missing? Bail. 707 | if (empty($plugin)) { 708 | return $images; 709 | } 710 | 711 | // Not a valid size passed in? Bail. 712 | if (!in_array($type, ['icon', 'banner', 'screenshot'], true)) { 713 | return $images; 714 | } 715 | 716 | // Set path and URL to this plugin's own images directory. 717 | $image_path = untrailingslashit(WP_PLUGIN_DIR).'/'.$plugin.'/images'; 718 | $image_url = untrailingslashit(WP_PLUGIN_URL).'/'.$plugin.'/images'; 719 | 720 | // Allow directory location to be filtered. 721 | $image_path = apply_filters('codepotent_update_manager_image_path', $image_path); 722 | $image_url = apply_filters('codepotent_update_manager_image_url', $image_url); 723 | 724 | // Banner and icon images are keyed differently; it's a core thing. 725 | $image_qualities = [ 726 | 'icon' => ['default', '1x', '2x'], 727 | 'banner' => ['default', 'low', 'high'], 728 | ]; 729 | 730 | // Array of dimensions for bannes and icons. 731 | $image_dimensions = [ 732 | 'icon' => ['default'=>'128', '1x'=>'128', '2x'=>'256'], 733 | 'banner' => ['default'=>'772x250', 'low'=>'772x250', 'high'=>'1544x500'], 734 | ]; 735 | 736 | // Handle icon and banner requests. 737 | if ($type === 'icon' || $type === 'banner') { 738 | // For SVG banners/icons; one tiny loop handles both. 739 | if (file_exists($image_path.'/'.$type.'.svg')) { 740 | foreach ($image_qualities[$type] as $key) { 741 | $images[$key] = $image_url.'/'.$type.'.svg'; 742 | } 743 | } 744 | // Ok, no svg. How about png or jpg? 745 | else { 746 | // This loop doesn't break early, so, it favors png. 747 | foreach (['jpg', 'png'] as $ext) { 748 | // Pop keys off the end of the $images_qualities array. 749 | $all_keys = $image_qualities[$type]; 750 | $last_key = array_pop($all_keys); 751 | $middle_key = array_pop($all_keys); 752 | // Normal size images found? Add them. 753 | if (file_exists($image_path.'/'.$type.'-'.$image_dimensions[$type][$middle_key].'.'.$ext)) { 754 | foreach ($image_qualities[$type] as $key) { 755 | $images[$key] = $image_url.'/'.$type.'-'.$image_dimensions[$type][$middle_key].'.'.$ext; 756 | } 757 | } 758 | // Retina image found? Add it. 759 | if (file_exists($image_path.'/'.$type.'-'.$image_dimensions[$type][$last_key].'.'.$ext)) { 760 | $images[$last_key] = $image_url.'/'.$type.'-'.$image_dimensions[$type][$last_key].'.'.$ext; 761 | } 762 | 763 | } // foreach 764 | 765 | } // inner if/else 766 | 767 | // Return icon or banner URLs. 768 | return $images; 769 | 770 | } 771 | 772 | // Oh, banners? Note these are from current version, not new version. 773 | if ($type === 'screenshot') { 774 | 775 | // Does /images/ directory exists? Prevent notices. 776 | if (file_exists($image_path)) { 777 | 778 | // Scan the directory. 779 | $dir_contents = scandir($image_path); 780 | 781 | // Capture only the screenshot URLs. 782 | foreach ($dir_contents as $name) { 783 | if (strpos(strtolower($name), 'screenshot') === 0) { 784 | $start = strpos($name, '-')+1; 785 | $for = strpos($name, '.')-$start; 786 | $screenshot_number = substr($name, $start, $for); 787 | $images[$screenshot_number] = $image_url.'/'.$name; 788 | } 789 | } 790 | 791 | // Proper the sort. 792 | ksort($images); 793 | 794 | } 795 | 796 | } 797 | 798 | // Return any screenshot URLs. 799 | return $images; 800 | 801 | } 802 | 803 | /** 804 | * Retrieve latest ClassicPress version number. 805 | * 806 | * @author John Alarcon 807 | * 808 | * @since 1.0.0 809 | * 810 | * @return string 811 | */ 812 | public function get_latest_version_number() { 813 | 814 | // Get current ClassicPress version, if stored. 815 | $version = get_transient('codepotent_update_manager_cp_version'); 816 | 817 | // Return version number, if now known. 818 | if (!empty($version)) { 819 | return $version; 820 | } 821 | 822 | // Make a request to the ClassicPress versions API. 823 | $response = wp_remote_get('https://api-v1.classicpress.net/upgrade/index.php', ['timeout'=>3]); 824 | 825 | // Problems? Bail. 826 | if (is_wp_error($response) || empty($response)) { 827 | return; 828 | } 829 | 830 | // Get decoded reponse. 831 | $versions = json_decode(wp_remote_retrieve_body($response)); 832 | 833 | // Reverse iterate to find the latest version. 834 | for ($i=count($versions)-1; $i>0; $i--) { 835 | if (!strpos($versions[$i], 'nightly')) { 836 | if (!strpos($versions[$i], 'alpha')) { 837 | if (!strpos($versions[$i], 'beta')) { 838 | if (!strpos($versions[$i], 'rc')) { 839 | $version = $versions[$i]; 840 | break; 841 | } 842 | } 843 | } 844 | } 845 | } // At this point, $version = 1.1.1.json 846 | 847 | // Get just the version portion of the string. 848 | if ($version) { 849 | $version = str_replace('.json', '', $version); 850 | } 851 | 852 | // A transient ensures the query is not run more than every 10 minutes. 853 | set_transient('codepotent_update_manager_cp_version', $version, MINUTE_IN_SECONDS * 10); 854 | 855 | // Return the version string. 856 | return $version; 857 | 858 | } 859 | 860 | } 861 | 862 | // Run it! 863 | UpdateClient::get_instance(); -------------------------------------------------------------------------------- /classes/index.php: -------------------------------------------------------------------------------- 1 | init(); 87 | 88 | // Process the error log into object properties. 89 | $this->convert_error_log_into_properties(); 90 | 91 | } 92 | 93 | /** 94 | * Plugin initialization 95 | * 96 | * Register actions and filters to hook the plugin into the system. 97 | * 98 | * @author John Alarcon 99 | * 100 | * @since 1.0.0 101 | */ 102 | public function init() { 103 | 104 | // Load constants. 105 | require_once plugin_dir_path(__FILE__).'includes/constants.php'; 106 | 107 | // Load plugin update class. 108 | require_once(PATH_INCLUDES.'/functions.php'); 109 | 110 | // Load plugin update class. 111 | require_once(PATH_CLASSES.'/UpdateClient.class.php'); 112 | 113 | // Update options in time to redirect; keeps admin bar alerts current. 114 | add_action('plugins_loaded', [$this, 'update_display_options']); 115 | 116 | // Execute purge requests; if no purge requested, nothing happens. 117 | add_action('plugins_loaded', [$this, 'process_purge_requests']); 118 | 119 | // Register admin page and menu item. 120 | add_action('admin_menu', [$this, 'register_admin_menu']); 121 | 122 | // Admin notices for purge confirmations. 123 | add_action('admin_notices', [$this, 'render_confirmation_notices']); 124 | 125 | // Enqueue global scripts. 126 | add_action('admin_enqueue_scripts', [$this, 'enqueue_global_scripts']); 127 | add_action('wp_enqueue_scripts', [$this, 'enqueue_global_scripts']); 128 | 129 | // Enqueue global styles. 130 | add_action('admin_enqueue_scripts', [$this, 'enqueue_global_styles']); 131 | add_action('wp_enqueue_scripts', [$this, 'enqueue_global_styles']); 132 | 133 | // Handle AJAX requests to purge the error log. 134 | add_action('wp_ajax_purge_error_log', [$this, 'process_ajax_purge_requests']); 135 | 136 | // Add error log link to admin bar. 137 | add_action('wp_before_admin_bar_render', [$this, 'register_admin_bar']); 138 | 139 | // Replace footer text with plugin name and version info. 140 | add_filter('admin_footer_text', [$this, 'filter_footer_text'], 10000); 141 | 142 | // Add a "Settings" link to core's plugin admin row. 143 | add_filter('plugin_action_links_'.PLUGIN_IDENTIFIER, [$this, 'register_action_links']); 144 | 145 | // Register hooks for activation, deactivation, and uninstallation. 146 | register_uninstall_hook(__FILE__, [__CLASS__, 'uninstall_plugin']); 147 | register_activation_hook(__FILE__, [$this, 'activate_plugin']); 148 | register_deactivation_hook(__FILE__, [$this, 'deactivate_plugin']); 149 | 150 | // POST-ADOPTION: Remove these actions before pushing your next update. 151 | add_action('upgrader_process_complete', [$this, 'enable_adoption_notice'], 10, 2); 152 | add_action('admin_notices', [$this, 'display_adoption_notice']); 153 | 154 | } 155 | 156 | // POST-ADOPTION: Remove this method before pushing your next update. 157 | public function enable_adoption_notice($upgrader_object, $options) { 158 | if ($options['action'] === 'update') { 159 | if ($options['type'] === 'plugin') { 160 | if (!empty($options['plugins'])) { 161 | if (in_array(plugin_basename(__FILE__), $options['plugins'])) { 162 | set_transient(PLUGIN_PREFIX.'_adoption_complete', 1); 163 | } 164 | } 165 | } 166 | } 167 | } 168 | 169 | // POST-ADOPTION: Remove this method before pushing your next update. 170 | public function display_adoption_notice() { 171 | if (get_transient(PLUGIN_PREFIX.'_adoption_complete')) { 172 | delete_transient(PLUGIN_PREFIX.'_adoption_complete'); 173 | echo '
The '.PLUGIN_NAME.' plugin has been officially adopted and is now managed by '.PLUGIN_AUTHOR.', a longstanding and trusted ClassicPress developer and community member. While it has been wonderful to serve the ClassicPress community with free plugins, tutorials, and resources for nearly 3 years, it\'s time that I move on to other endeavors. This notice is to inform you of the change, and to assure you that the plugin remains in good hands. I\'d like to extend my heartfelt thanks to you for making my plugins a staple within the community, and wish you great success with ClassicPress!
'; 176 | echo 'All the best!
'; 177 | echo '~ John Alarcon (Code Potent)
'; 178 | echo ''.esc_html__('Error log has been emptied.', 'codepotent-php-error-log-viewer').'
'; 706 | $markup .= ''.esc_html__('Your PHP error log could not be found.', 'codepotent-php-error-log-viewer').'
';
1117 | $markup .= sprintf(
1118 | esc_html__('Open your %1$swp-config.php%2$s file and find the line that reads %1$sdefine(\'WP_DEBUG\', false);%2$s. Replace that single line with all of the following lines. Be sure to change the path to reflect the location of your PHP error log file. You can (and should) place your error log file outside your publicly accessible web directory.', 'codepotent-php-error-log-viewer'),
1119 | '',
1120 | '
'
1121 | );
1122 | $markup .= '
¯\_(ツ)_/¯
'; 1134 | 1135 | // Plugin container. 1136 | $markup .= ''.str_replace(["\r","\n"], ''; 108 | 109 | // Send the whole affair off to the error log. 110 | trigger_error($msg, $error_level); 111 | 112 | } -------------------------------------------------------------------------------- /includes/index.php: -------------------------------------------------------------------------------- 1 | a { 209 | text-decoration: none; 210 | } 211 | #footer-thankyou, 212 | #footer-thankyou > a { 213 | font-style:normal; 214 | } -------------------------------------------------------------------------------- /styles/index.php: -------------------------------------------------------------------------------- 1 |
', print_r($data, true)).'