├── 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 | ![PHP Error Log Viewer plugin for ClassicPress by Code Potent](https://codepotent.com/wp-content/uploads/2021/03/codepotent-php-error-log-viewer-plugin-for-classicpress-image-05.png) 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 | ![PHP Error Log Viewer plugin for ClassicPress](https://codepotent.com/wp-content/uploads/2020/02/codepotent-php-error-log-viewer-plugin-for-classicpress-screenshot-02.png) 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 | ![PHP Error Log Viewer plugin for ClassicPress](https://codepotent.com/wp-content/uploads/2020/02/codepotent-php-error-log-viewer-plugin-for-classicpress-screenshot-03.png) 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 | ![PHP Error Log Viewer plugin for ClassicPress](https://codepotent.com/wp-content/uploads/2019/08/codepotent-php-error-log-viewer-plugin-for-classicpress-image-02.png) 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 | ![PHP Error Log Viewer plugin for ClassicPress](https://codepotent.com/wp-content/uploads/2019/08/codepotent-php-error-log-viewer-plugin-for-classicpress-image-02.png) 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 | ![PHP Error Log Viewer plugin for ClassicPress](https://codepotent.com/wp-content/uploads/2021/03/codepotent-php-error-log-viewer-plugin-for-classicpress-image-04.png) 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 = '

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 |
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 |
function yourprefix_after_error_log_legend($markup) {
130 |     $markup = '

This 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 |
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's 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 '
'; 174 | echo '

IMPORTANT information about the '.PLUGIN_NAME.' plugin

'; 175 | 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 '
'; 179 | } 180 | } 181 | 182 | /** 183 | * Admin bar link 184 | * 185 | * Add a link to the admin bar that leads to the PHP error log; just a minor 186 | * convenience. 187 | * 188 | * @author John Alarcon 189 | * 190 | * @since 2.0.0 191 | * 192 | */ 193 | public function register_admin_bar() { 194 | 195 | // Admins only. 196 | if (!current_user_can('manage_options')) { 197 | return; 198 | } 199 | 200 | // Primary link text. 201 | $link_text = esc_html__('PHP Errors', 'codepotent-php-error-log-viewer'); 202 | 203 | // Alert bubble for displayed errors. 204 | $primary_alert = ''; 205 | if ($this->errors_displayed > 0) { 206 | $primary_alert = ''.number_format($this->errors_displayed).''; 207 | } 208 | 209 | // Alert bubble for hidden errors. 210 | $secondary_alert = ''; 211 | if ($this->error_count !== $this->errors_displayed) { 212 | $secondary_alert = ''.number_format($this->error_count-$this->errors_displayed).''; 213 | } 214 | 215 | // Filters to remove alert bubbles. 216 | $primary_alert = apply_filters(PLUGIN_PREFIX.'_primary_alert', $primary_alert); 217 | $secondary_alert = apply_filters(PLUGIN_PREFIX.'_secondary_alert', $secondary_alert); 218 | 219 | // Assemble the return URL. 220 | $return_url = $_SERVER['REQUEST_SCHEME'].'://'.$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI']; 221 | 222 | // Bring the admin bar into scope. 223 | global $wp_admin_bar; 224 | 225 | // Add the main link. 226 | $wp_admin_bar->add_menu([ 227 | 'parent' => false, 228 | 'id' => PLUGIN_PREFIX.'_admin_bar', 229 | 'title' => $link_text.$primary_alert.$secondary_alert, 230 | 'href' => admin_url('tools.php?page='.PLUGIN_SHORT_SLUG), 231 | 'meta' => [ 232 | 'title' => sprintf( 233 | esc_html__('PHP %s', 'codepotent-php-error-log-viewer'), 234 | phpversion() 235 | ) 236 | ] 237 | ]); 238 | 239 | // Add submenu item to purge log via AJAX; only if log is writeable. 240 | if (is_writable($this->error_log)) { 241 | $wp_admin_bar->add_menu([ 242 | 'parent' => PLUGIN_PREFIX.'_admin_bar', 243 | 'title' => __('Purge Error Log'), 244 | 'id' => PLUGIN_SLUG.'-admin-bar-purge-link', 245 | 'href' => '#', 246 | 'meta' => [ 247 | 'data-return-url' => esc_url($return_url) 248 | ] 249 | ]); 250 | } 251 | } 252 | 253 | /** 254 | * Register admin view 255 | * 256 | * Place a "PHP Error Log" submenu item under the core Tools menu. This also 257 | * registers the admin page for same. 258 | * 259 | * @author John Alarcon 260 | * 261 | * @since 1.0.0 262 | */ 263 | public function register_admin_menu() { 264 | 265 | // Add submenu under the Tools menu. 266 | add_submenu_page( 267 | 'tools.php', 268 | esc_html__('PHP Error Log', 'codepotent-php-error-log-viewer'), 269 | PLUGIN_MENU_TEXT, 270 | 'manage_options', 271 | PLUGIN_SHORT_SLUG, 272 | [$this, 'render_php_error_log'] 273 | ); 274 | 275 | } 276 | 277 | /** 278 | * Add a direct link to the PHP Error Log in the plugin admin display 279 | * 280 | * @author John Alarcon 281 | * 282 | * @since 1.0.0 283 | * 284 | * @param array $links Administration links for the plugin. 285 | * 286 | * @return array $links Updated administration links. 287 | */ 288 | public function register_action_links($links) { 289 | 290 | // Prepend error log link in plugin row; for admins only. 291 | if (current_user_can('manage_options')) { 292 | $error_log_link = ''.esc_html__('PHP Error Log', 'codepotent-php-error-log-viewer').''; 293 | array_unshift($links, $error_log_link); 294 | } 295 | 296 | // Return the maybe-updated $links array. 297 | return $links; 298 | 299 | } 300 | 301 | /** 302 | * Enqueue global scripts 303 | * 304 | * This method enqueues scripts that are used by both admin and user sides. 305 | * 306 | * @author John Alarcon 307 | * 308 | * @since 1.0.0 309 | */ 310 | public function enqueue_global_scripts() { 311 | 312 | // Admins only. 313 | if (!current_user_can('manage_options')) { 314 | return; 315 | } 316 | 317 | // Applies for all admin views. 318 | wp_enqueue_script(PLUGIN_SLUG.'-global', URL_SCRIPTS.'/global.js', ['jquery']); 319 | 320 | // For redirecting back to the current page. 321 | $redirect_target = (is_ssl()?'https://':'http://').$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI']; 322 | 323 | // Setup a deletion-link URL. 324 | $deletion_link = esc_url( 325 | wp_nonce_url( 326 | admin_url('tools.php?page='.PLUGIN_SHORT_SLUG.'&purge_errors=1&redirect_url='.$redirect_target), 327 | PLUGIN_PREFIX.'_purge_error_log' 328 | ) 329 | ); 330 | 331 | // Data to localize to JavaScript. 332 | $localized_data = [ 333 | 'prefix' => PLUGIN_SLUG, 334 | 'ajax_url' => admin_url('admin-ajax.php'), 335 | 'ajax_nonce' => wp_create_nonce('purge_error_log'), 336 | 'deletion_link' => $deletion_link, 337 | 'text_confirmation' => esc_html('Remove all entries from the PHP error log?', 'codepotent-php-error-log-viewer'), 338 | 'text_zero_bytes' => esc_html('0 bytes', 'codepotent-php-error-log-viewer'), 339 | 'text_ajax_success' => esc_html('Error log successfully purged.', 'codepotent-php-error-log-viewer'), 340 | 'text_ajax_failure' => esc_html('Something went wrong; error log was not purged.', 'codepotent-php-error-log-viewer'), 341 | ]; 342 | 343 | // Scope the above PHP array out to the JS file. 344 | wp_localize_script(PLUGIN_SLUG.'-global', PLUGIN_PREFIX.'_data', $localized_data); 345 | 346 | } 347 | 348 | /** 349 | * Enqueue global styles 350 | * 351 | * As of version 2.0.0, the plugin integrates admin bar alerts. An admin bar 352 | * can be present in both (or either of) the front and back ends, so, styles 353 | * for the alert bubble must be present on both sides. Still, the styles are 354 | * only needed if the admin bar is visible, so, we use that as the check and 355 | * squeeze another bit of performance out of the plugin. Note that the style 356 | * sheet (at this writing) is only 7k, so, it's not a huge savings; still, a 357 | * saved request never hurts. 358 | * 359 | * @author John Alarcon 360 | * 361 | * @since 2.2.0 362 | */ 363 | public function enqueue_global_styles() { 364 | 365 | // Admins only. 366 | if (!current_user_can('manage_options')) { 367 | return; 368 | } 369 | 370 | // Used in admin bar alerts; enqueue for all needed pages. 371 | if (is_admin_bar_showing()) { 372 | wp_enqueue_style(PLUGIN_SLUG.'-admin', URL_STYLES.'/global.css'); 373 | } 374 | 375 | } 376 | 377 | /** 378 | * Get error type. 379 | * 380 | * This method receives a line from the error log and determines the type of 381 | * error it is. 382 | * 383 | * @author John Alarcon 384 | * 385 | * @since 2.0.0 386 | * 387 | * @param string $error A line from the error log. 388 | * 389 | * @return string The type of error it is. 390 | */ 391 | public function get_error_type($error) { 392 | 393 | // Run through various acts of string-fu. 394 | if (strpos($error, 'PHP Deprecated')) { 395 | $type = 'deprecated'; 396 | } else if (strpos($error, 'PHP Notice')) { 397 | $type = 'notice'; 398 | } else if (substr($error, 0, 11) === 'Stack trace' || strpos($error, 'Stack trace')) { 399 | $type = 'stack_trace_title'; 400 | } else if (substr($error, 0, 1) === '#' || strpos($error, 'stderr: #') || preg_match('|( PHP +[0-9]+\. )|', $error)) { 401 | $type = 'stack_trace_step'; 402 | } else if (substr($error, 0, 9) === 'thrown in' || strpos($error, 'thrown in')) { 403 | $type = 'stack_trace_origin'; 404 | } else if (strpos($error, 'error:') || strpos($error, 'stderr:') || strpos($error, '[error]')) { 405 | $type = 'error'; 406 | } else if (strpos($error, 'PHP Warning') || strpos($error, '[warn]')) { 407 | $type = 'warning'; 408 | } else if (strpos($error, '(') === 0 || strpos($error, ')') === 0 || strpos($error, '[') === 0 || strpos($error, ']') === 0 || strpos($error, 'in ') === 0 || empty($error)) { 409 | $type = 'stack_trace_step'; 410 | } else { 411 | $type = 'other'; 412 | } 413 | 414 | // Return the error type. 415 | return $type; 416 | 417 | } 418 | 419 | /** 420 | * Get error types 421 | * 422 | * This method returns an array of error types contemplated by the plugin. 423 | * 424 | * @author John Alarcon 425 | * 426 | * @since 1.0.0 427 | * 428 | * @return array Error type texts keyed accordingly. 429 | */ 430 | public function get_error_types() { 431 | 432 | // Array of error type texts keyed by type. 433 | $error_types = [ 434 | 'deprecated' => esc_html__('Deprecated', 'codepotent-php-error-log-viewer'), 435 | 'notice' => esc_html__('Notice', 'codepotent-php-error-log-viewer'), 436 | 'warning' => esc_html__('Warning', 'codepotent-php-error-log-viewer'), 437 | 'error' => esc_html__('Error', 'codepotent-php-error-log-viewer'), 438 | 'stack_trace_title' => esc_html__('Stack Trace', 'codepotent-php-error-log-viewer'), 439 | 'stack_trace_step' => '', 440 | 'stack_trace_origin' => '', 441 | 'other' => esc_html__('Other', 'codepotent-php-error-log-viewer'), 442 | ]; 443 | 444 | // Return the error types. 445 | return $error_types; 446 | 447 | } 448 | 449 | /** 450 | * Get error defaults 451 | * 452 | * This method is used to ensure all expected elements are initialized. 453 | * 454 | * @author John Alarcon 455 | * 456 | * @since 1.0.0 457 | * 458 | * @return array[] 459 | */ 460 | public function get_error_defaults() { 461 | 462 | // Setup an array of empty arrays as defaults. 463 | $defaults = []; 464 | foreach (array_keys($this->get_error_types()) as $type) { 465 | $defaults[$type] = []; 466 | } 467 | 468 | // Return the defaults array. 469 | return $defaults; 470 | 471 | } 472 | 473 | /** 474 | * Process error log 475 | * 476 | * This method processes the error log into various object properties. 477 | * 478 | * @author John Alarcon 479 | * 480 | * @since 2.0.0 481 | * 482 | * @return void 483 | */ 484 | public function convert_error_log_into_properties() { 485 | 486 | // Admins only. 487 | if (!current_user_can('manage_options')) { 488 | return; 489 | } 490 | 491 | // Initialization. 492 | $this->errors = $this->raw_errors = []; 493 | 494 | // Error log not found? Bail. 495 | if (!file_exists($error_log = ini_get('error_log'))) { 496 | return; 497 | } 498 | 499 | // Set the error log path. 500 | $this->error_log = $error_log; 501 | 502 | // Set the filesize; in bytes. 503 | $this->filesize = filesize($error_log); 504 | 505 | // Set a default errors array. 506 | $this->errors = $this->get_error_defaults(); 507 | 508 | // Set plugin options. 509 | $this->options = get_option(PLUGIN_PREFIX, []); 510 | 511 | // If no errors found, this is far enough. 512 | if (empty($this->raw_errors = file($error_log))) { 513 | return; 514 | } 515 | 516 | // Reverse sort the array, if requested. 517 | if (!empty($this->options['reverse_sort']) && $this->options['reverse_sort']) { 518 | $this->reverse_sort_errors(); 519 | } 520 | 521 | // Iterate over error lines. 522 | foreach ($this->raw_errors as $n=>$error) { 523 | // Tidy up the ends. 524 | $error = trim($error); 525 | // Determine this error's type. 526 | $type = $this->get_error_type($error); 527 | // Map the error to a type in all cases. 528 | $this->error_map[$n] = $type; 529 | // Capture only those errors that will be displayed. 530 | if (!empty($this->options[$type])) { 531 | $this->errors[$type][$n] = $error; 532 | } 533 | // For user-generated errors, remove the rogue line at the bottom. 534 | if (strstr($error, 'User Generated')) { 535 | $error = preg_replace('|(<\/pre> in )(.*)|', '', $error); 536 | } 537 | // Bold (most) error titles. 538 | $this->errors[$type][$n] = preg_replace('|(PHP )([A-Za-z]){1,} *([A-Za-z ]){1,}|', '${0}', $error); 539 | // Strip: "mod_fcgid: stderr:" 540 | $this->errors[$type][$n] = str_replace('mod_fcgid: stderr: ', '', $this->errors[$type][$n]); 541 | // Regex to find a datetime string. 542 | $pattern = '|([){1}([A-Za-z0-9_ -:\/]){1,}(]){1}|'; 543 | // Strip date/time, or wrap it for styling purposes. 544 | if (empty($this->options['datetime'])) { 545 | $this->errors[$type][$n] = preg_replace($pattern, '', $this->errors[$type][$n]); 546 | } else { 547 | $this->errors[$type][$n] = preg_replace($pattern, '${0}', $this->errors[$type][$n]); 548 | } 549 | } 550 | 551 | // With errors all gathered and sorted, count them up. 552 | foreach ($this->errors as $type=>$error_array) { 553 | // Stack trace data isn't counted; parent errors are. 554 | if (strpos($type, 'stack_trace_') !== 0) { 555 | // Count errors to be displayed. 556 | if (!empty($this->options[$type])) { 557 | $this->errors_displayed += count($error_array); 558 | } 559 | // Total of all errors. 560 | $this->error_count += count($error_array); 561 | } 562 | } 563 | 564 | } 565 | 566 | /** 567 | * Purge error log 568 | * 569 | * @author John Alarcon 570 | * 571 | * @since 1.0.0 572 | */ 573 | public function process_purge_requests() { 574 | 575 | // Admins only. 576 | if (!current_user_can('manage_options')) { 577 | return; 578 | } 579 | 580 | // No nonce? Bail. 581 | if (!isset($_GET['_wpnonce'])) { 582 | return; 583 | } 584 | 585 | // Suspicious nonce? Bail. 586 | if (!wp_verify_nonce($_GET['_wpnonce'], PLUGIN_PREFIX.'_purge_error_log')) { 587 | return; 588 | } 589 | 590 | // Not requesting purge? Bail. 591 | if (!isset($_GET['purge_errors']) || !$_GET['purge_errors']) { 592 | return; 593 | } 594 | 595 | // Overwrite log file with 0 bytes; set transient. 596 | if (!empty($this->error_log) && is_writable($this->error_log)) { 597 | if (file_put_contents($this->error_log, '') !== false) { 598 | set_transient(PLUGIN_PREFIX.'_purged', 1, 120); 599 | } 600 | } 601 | 602 | // In case we need a custom redirect. 603 | $redirect_url = admin_url('tools.php?page='.PLUGIN_SHORT_SLUG); 604 | if (!empty($_GET['redirect_url'])) { 605 | $redirect_url = esc_url($_GET['redirect_url']); 606 | } 607 | 608 | // Redirect. 609 | wp_safe_redirect($redirect_url); 610 | exit; 611 | 612 | } 613 | 614 | /** 615 | * Purge error log via AJAX request. 616 | * 617 | * @author John Alarcon 618 | * 619 | * @since 2.2.0 620 | */ 621 | public function process_ajax_purge_requests() { 622 | 623 | // Admins only. 624 | if (!current_user_can('manage_options')) { 625 | return; 626 | } 627 | 628 | // If nonce checks out, purge the error log. 629 | if (check_ajax_referer( 'purge_error_log' )) { 630 | if (!empty($this->error_log) && is_writable($this->error_log)) { 631 | file_put_contents($this->error_log, ''); 632 | } 633 | } 634 | 635 | // ...and that's that. 636 | wp_die(); 637 | 638 | } 639 | 640 | /** 641 | * Update filter options 642 | * 643 | * This method updates the plugin's settings made with the checkboxes at the 644 | * top of the display; for filtering the displayed errors. 645 | * 646 | * @author John Alarcon 647 | * 648 | * @since 1.0.0 649 | * 650 | * @return boolean 651 | */ 652 | public function update_display_options() { 653 | 654 | // Define the nonce name. 655 | $nonce_name = PLUGIN_PREFIX.'_nonce'; 656 | 657 | // If nonce is missing, bail. 658 | if (empty($_POST[$nonce_name])) { 659 | return false; 660 | } 661 | 662 | // If nonce is suspect, bail. 663 | if (!wp_verify_nonce($_POST[$nonce_name], $nonce_name)) { 664 | return false; 665 | } 666 | 667 | // Date is a display option, not an error type; prepend it manually. 668 | $this->options['datetime'] = (isset($_POST[PLUGIN_PREFIX]['datetime'])) ? 1 : ''; 669 | 670 | // Gather display options; ensure clean values. 671 | foreach (array_keys($this->get_error_types()) as $type) { 672 | $this->options[$type] = (isset($_POST[PLUGIN_PREFIX][$type])) ? 1 : ''; 673 | } 674 | 675 | // More stack trace properties; mirrored from stack trace title setting. 676 | $this->options['stack_trace_step'] = (!empty($this->options['stack_trace_title'])) ? 1 : ''; 677 | $this->options['stack_trace_origin'] = (!empty($this->options['stack_trace_title'])) ? 1 : ''; 678 | 679 | // Sorting is a display option, not an error type; append it manually. 680 | $this->options['reverse_sort'] = (isset($_POST[PLUGIN_PREFIX]['reverse_sort'])) ? 1 : ''; 681 | 682 | // Update options. 683 | update_option(PLUGIN_PREFIX, $this->options); 684 | 685 | // Redirect to ensure admin bar alerts show correct integer(s). 686 | wp_safe_redirect(admin_url('tools.php?page='.PLUGIN_SHORT_SLUG)); 687 | exit; 688 | 689 | } 690 | 691 | /** 692 | * Render success message 693 | * 694 | * This is only used for a "traditional" log purge; that is, a purge that is 695 | * done via traditional URL, rather than AJAX. 696 | * 697 | * @author John Alarcon 698 | * 699 | * @since 1.0.0 700 | */ 701 | public function markup_success_message() { 702 | 703 | // Assemble a dismissible message. 704 | $markup = '
'; 705 | $markup .= '

'.esc_html__('Error log has been emptied.', 'codepotent-php-error-log-viewer').'

'; 706 | $markup .= '
'."\n"; 707 | 708 | // Delete the transient that triggered this message. 709 | delete_transient(PLUGIN_PREFIX.'_purged'); 710 | 711 | // Return the markup string. 712 | return $markup; 713 | 714 | } 715 | 716 | /** 717 | * Markup filter inputs 718 | * 719 | * @author John Alarcon 720 | * 721 | * @since 1.0.0 722 | * 723 | * @return string HTML form for filtering the errors displayed. 724 | */ 725 | public function markup_display_inputs() { 726 | 727 | // Form open. 728 | $markup = '
'; 729 | 730 | // Markup the nonce field. 731 | $markup .= wp_nonce_field(PLUGIN_PREFIX.'_nonce', PLUGIN_PREFIX.'_nonce', true, false); 732 | 733 | // Date/time input. 734 | $markup .= ''; 735 | 736 | // Divider. 737 | $markup .= ''; 738 | 739 | // Error type texts are translated/escaped here, not in the loop below. 740 | $error_types = $this->get_error_types(); 741 | 742 | // Print the labels and inputs. 743 | foreach ($error_types as $type=>$text) { 744 | $total = !empty($this->errors[$type]) ? count($this->errors[$type]) : 0; 745 | if (strpos($type, 'stack_trace') !== 0) { 746 | $markup .= ''; 747 | } 748 | } 749 | 750 | // Divider. 751 | $markup .= ''; 752 | 753 | // Sort input. 754 | $markup .= ''; 758 | 759 | // Divider. 760 | $markup .= ''; 761 | 762 | // Sort input. 763 | $markup .= ''; 767 | 768 | // Markup the submit button. 769 | $markup .= ''; 770 | 771 | // Close the form. 772 | $markup .= '
'; 773 | 774 | // Return markup string. 775 | return $markup; 776 | 777 | } 778 | 779 | /** 780 | * Markup jump links 781 | * 782 | * Because new errors always appear at the bottom, if the error log has many 783 | * entries, the user would have to scroll each time the page was loaded. The 784 | * jump-links allow users to easily jump from the top to the bottom and back 785 | * again without the need for endless scrolling. These links only display if 786 | * there are enough entries showing onscreen. 787 | * 788 | * @author John Alarcon 789 | * 790 | * @since 1.0.0 791 | * 792 | * @param string $where Set to "header" if not "footer". 793 | * 794 | * @return string Any generated markup. 795 | */ 796 | public function markup_jump_link($where) { 797 | 798 | // Initialization. 799 | $markup = ''; 800 | 801 | // Not many errors currently displaying? Bail. 802 | if ($this->errors_displayed < 10) { 803 | return $markup; 804 | } 805 | 806 | // Container. 807 | $markup .= '
'; 808 | 809 | // Markup the jump depending on whether it's for the header or footer. 810 | if ($where === 'header') { 811 | $markup .= ''.esc_html__('Skip to bottom', 'codepotent-php-error-log-viewer').''; 812 | } else { 813 | $markup .= ''.esc_html__('Back to top', 'codepotent-php-error-log-viewer').''; 814 | } 815 | 816 | // Container. 817 | $markup .= '
'; 818 | 819 | // Return markup string. 820 | return $markup; 821 | 822 | } 823 | 824 | /** 825 | * Markup action buttons 826 | * 827 | * Generates markup for the buttons used to refresh and purge the error log. 828 | * This is always used at the top of the display. If there are enough errors 829 | * that the page begins to scroll, the buttons will also be placed below the 830 | * list to convenience. 831 | * 832 | * @author John Alarcon 833 | * 834 | * @since 1.0.0 835 | * 836 | * @param $where string Location, top or bottom. 837 | * 838 | * @return string HTML markup for refresh and purge buttons. 839 | */ 840 | public function markup_action_buttons($where) { 841 | 842 | if ($where !== 'top') { 843 | $where = 'bottom'; 844 | } 845 | 846 | // Open containers. 847 | $markup = '
'; 848 | $markup .= ''; 849 | 850 | // Refresh button. 851 | $markup .= ''.esc_html__('Refresh Error Log', 'codepotent-php-error-log-viewer').''; 852 | 853 | // Purge button; if log is writeable. 854 | if (is_writable($this->error_log)) { 855 | $markup .= ''.esc_html__('Purge Error Log', 'codepotent-php-error-log-viewer').''; 856 | } 857 | 858 | // Close containers. 859 | $markup .= ''; 860 | $markup .= '
'; 861 | 862 | // Return the string. 863 | return $markup; 864 | 865 | } 866 | 867 | /** 868 | * Markup error log size 869 | * 870 | * @author John Alarcon 871 | * 872 | * @since 1.0.0 873 | * 874 | * @deprecated As of 2.2.0, use markup_filesize_location_indicator instead. 875 | * 876 | * @return string Text representation, ie, 123 bytes, 10.3 kB, 1.2 MB 877 | */ 878 | public function markup_filesize_indicator() { 879 | 880 | return $this->markup_filesize_location_indicator(); 881 | 882 | } 883 | 884 | /** 885 | * Markup error log size and location. 886 | * 887 | * @author John Alarcon 888 | * 889 | * @since 2.2.0 890 | * 891 | * @return string Text representation, ie, 123 bytes, 10.3 kB, 1.2 MB 892 | */ 893 | public function markup_filesize_location_indicator() { 894 | 895 | // Cast the log size. 896 | settype($this->filesize, 'int'); 897 | 898 | // Setup default display text. 899 | $display_text = sprintf( 900 | esc_html__('%d bytes', 'codepotent-php-error-log-viewer'), 901 | $this->filesize 902 | ); 903 | 904 | // Is error log greater than 1MB? Change the text to suit. 905 | if ($this->filesize > 1000000) { 906 | $display_text = sprintf( 907 | esc_html__('%d MB', 'codepotent-php-error-log-viewer'), 908 | round($this->filesize/1000000, 1) 909 | ); 910 | } 911 | // Is error log greater than 1kB? Change the text to suit. 912 | else if ($this->filesize > 1000) { 913 | $display_text = sprintf( 914 | esc_html__('%d kB', 'codepotent-php-error-log-viewer'), 915 | round($this->filesize/1000, 1) 916 | ); 917 | } 918 | 919 | // Markup file location and filesize. 920 | $markup = '
'; 921 | $markup .= ''.esc_html__('Log File', 'codepotent-php-error-log-viewer').': '.$this->error_log.''; 922 | $markup .= '   '; 923 | $markup .= ''.esc_html__('Log Size', 'codepotent-php-error-log-viewer').': '.$display_text.''; 924 | $markup .= '
'; 925 | 926 | // Return the string. 927 | return $markup; 928 | 929 | } 930 | 931 | /** 932 | * Markup information legend 933 | * 934 | * @author John Alarcon 935 | * 936 | * @since 1.0.0 937 | * 938 | * @return string Markup for the legend. 939 | */ 940 | public function markup_legend() { 941 | 942 | // Error types. 943 | $types = $this->get_error_types(); 944 | 945 | // Open container. 946 | $markup = '
'; 947 | 948 | // Title. 949 | $markup .= '

'.esc_html__('Legend', 'codepotent-php-error-log-viewer').'

'; 950 | 951 | // Markup each legend item. 952 | foreach ($types as $type=>$text) { 953 | if ($type !== 'stack_trace_step' && $type !== 'stack_trace_origin') { 954 | $markup .= '
'.$text.'
'; 955 | } 956 | } 957 | 958 | // Close container. 959 | $markup .= '
'."\n"; 960 | 961 | // Return the markup. 962 | return $markup; 963 | 964 | } 965 | 966 | /** 967 | * Markup error rows 968 | * 969 | * This method handles markup generation for the error entries. 970 | * 971 | * @author John Alarcon 972 | * 973 | * @since 1.0.0 974 | * 975 | * @param array $raw_errors All errors as read from the log file. 976 | * @param array $typed_errors[type][line] Line numbers keyed by error type. 977 | * 978 | * @return string|mixed 979 | */ 980 | public function markup_error_rows() { 981 | 982 | // Initialize the markup string. 983 | $markup = ''; 984 | 985 | // Iterate over raw_errors array. 986 | foreach (array_keys($this->raw_errors) as $n) { 987 | 988 | // Get error type from its line position. 989 | $type = $this->error_map[$n]; 990 | 991 | // Not currently displaying this type of error? Next! 992 | if (empty($this->options[$type])) { 993 | continue; 994 | } 995 | 996 | /** 997 | * Stack trace titles are padded to make sure they "touch" the error 998 | * that produced them. If stack traces are displayed and errors have 999 | * been supressed from display, this block ensures that the rows are 1000 | * separated appropriately. 1001 | */ 1002 | $style = ''; 1003 | if ($type === 'stack_trace_title' && !$this->options['error']) { 1004 | $style = ' style="margin-top:13px;"'; 1005 | } 1006 | 1007 | // Mark up the error row. 1008 | $markup .= '
'; 1009 | $markup .= $this->errors[$type][$n]; 1010 | $markup .= '
'."\n"; 1011 | 1012 | } 1013 | 1014 | // Return the string. 1015 | return $markup; 1016 | 1017 | } 1018 | 1019 | /** 1020 | * Reverse sort errors 1021 | * 1022 | * Reversing the display order of errors in the log is more complicated than 1023 | * it seems on the surface. It's the stack trace data that screws everything 1024 | * up – simply reversing the array means the stack traces are then contained 1025 | * in the array above their respective errors and things break down. To sort 1026 | * the entries in reverse order, the stack trace data must first be removed, 1027 | * saved aside in a temp variable, then the remaining (actual) errors sorted 1028 | * while preserving their line position values. From there, the stack traces 1029 | * are re-added back to the mix, in preserved order and having been keyed to 1030 | * the particular error line to which they apply with some creative keywork. 1031 | * Since those newly keyed items will be at the end of the array, (and would 1032 | * display at the end of the error list,) a final reverse sort is applied. I 1033 | * am only bothering to explain this here because when someone sees the code 1034 | * it took to achieve this, there is going to be some 'splaining to do. Hey, 1035 | * if you have a better solution, I'm all ears! :) 1036 | * 1037 | * @author John Alarcon 1038 | * 1039 | * @since 2.0.0 1040 | */ 1041 | public function reverse_sort_errors() { 1042 | 1043 | // Initialization. 1044 | $stack_trace_parts = $actual_errors = []; 1045 | 1046 | // Key for reuniting stack trace data with parent errors after sort. 1047 | $error_line_number = 0; 1048 | 1049 | // Iterate over the raw lines read in from the error log. 1050 | foreach ($this->raw_errors as $n=>$error) { 1051 | 1052 | // Trim any split ends off the line. 1053 | $error = trim($error); 1054 | 1055 | // Get the error's type. 1056 | $type = $this->get_error_type($error); 1057 | 1058 | // If dealing with stack trace data, capture it; move on. 1059 | if (strpos($type, 'stack_trace') === 0) { 1060 | $stack_trace_parts[$error_line_number][$n] = $error; 1061 | continue; 1062 | } 1063 | 1064 | // Capture everyting else (ie, not stack trace data) as an error. 1065 | $actual_errors[$n] = $error; 1066 | 1067 | // Update the key to ensure stack trace data stays in sync. 1068 | $error_line_number = $n; 1069 | 1070 | } 1071 | 1072 | // Sort the now-stack-trace-free array; preserve keys. 1073 | krsort($actual_errors, SORT_NUMERIC); 1074 | 1075 | // Iterate over the stack trace data that was captured. 1076 | $i = 0; 1077 | foreach ($stack_trace_parts as $error_line=>$errors) { 1078 | // Rekey stack traces to fall under related errors in the array. 1079 | foreach ($errors as $error) { 1080 | $i += .05; 1081 | $actual_errors[(string)($error_line-$i)] = $error; 1082 | } 1083 | } 1084 | 1085 | // Newly keyed items are at the bottom; resort, preserving keys. 1086 | krsort($actual_errors, SORT_NUMERIC); 1087 | 1088 | // And set the whole affair back to the object. 1089 | $this->raw_errors = $actual_errors; 1090 | 1091 | } 1092 | 1093 | /** 1094 | * Provide notice and possible solutions if error log not found 1095 | * 1096 | * @author John Alarcon 1097 | * 1098 | * @since 1.0.0 1099 | */ 1100 | public function markup_error_log_404() { 1101 | 1102 | // Core container. 1103 | $markup = '
'; 1104 | 1105 | // Plugin container. 1106 | $markup .= '
'; 1107 | 1108 | // Title. 1109 | $markup .= '

'.esc_html__('PHP Error Log', 'codepotent-php-error-log-viewer').'

'; 1110 | 1111 | // Description of issue. 1112 | $markup .= '

'.esc_html__('Your PHP error log could not be found.', 'codepotent-php-error-log-viewer').'

'; 1113 | 1114 | // Probable solution. 1115 | $markup .= '

'.esc_html__('Possible Solution', 'codepotent-php-error-log-viewer').'

'; 1116 | $markup .= '

'; 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 .= '

'; 1123 | $markup .= '

'; 1130 | 1131 | // No dice? Maybe try .htaccess? 1132 | $markup .= '

'.esc_html__('Still not working?', 'codepotent-php-error-log-viewer').'

'; 1133 | $markup .= '

¯\_(ツ)_/¯

'; 1134 | 1135 | // Plugin container. 1136 | $markup .= '
'; 1137 | 1138 | // Core container. 1139 | $markup .= '
'; 1140 | 1141 | // Return the markup string. 1142 | return $markup; 1143 | 1144 | } 1145 | 1146 | /** 1147 | * Render confirmation notices 1148 | * 1149 | * This method renders the success and failure messages that are shown after 1150 | * the log is purged with AJAX via the admin bar link. These notices are for 1151 | * use in every admin view since the log can be deleted while on any screen. 1152 | * 1153 | * @author John Alarcon 1154 | * 1155 | * @since 2.2.0 1156 | */ 1157 | public function render_confirmation_notices() { 1158 | 1159 | // Success message. 1160 | echo ''; 1164 | 1165 | // Failure message. 1166 | echo ''; 1170 | 1171 | } 1172 | 1173 | /** 1174 | * Render PHP errors 1175 | * 1176 | * @author John Alarcon 1177 | * 1178 | * @since 1.0.0 1179 | * 1180 | */ 1181 | public function render_php_error_log() { 1182 | 1183 | // No permission to see the log? Bail. 1184 | if (!current_user_can('manage_options')) { 1185 | return; 1186 | } 1187 | 1188 | // Can't find error log? Describe a possible solution; return early. 1189 | if (!$this->error_log) { 1190 | echo $this->markup_error_log_404(); 1191 | return; 1192 | } 1193 | 1194 | // Outer container. 1195 | echo '
'; 1196 | 1197 | // Display success message if error log was just purged. 1198 | if (get_transient(PLUGIN_PREFIX.'_purged')) { 1199 | echo $this->markup_success_message(); 1200 | } 1201 | 1202 | // Print plugin title. 1203 | echo '

'.esc_html__('PHP Error Log', 'codepotent-php-error-log-viewer').'

'; 1204 | 1205 | // Print filter checkboxes. 1206 | echo $this->markup_display_inputs($this->errors, $this->options); 1207 | 1208 | // Print a jump-link in the header. 1209 | echo $this->markup_jump_link('header'); 1210 | 1211 | // Print buttons for refresh and purge actions. 1212 | echo $this->markup_action_buttons('top'); 1213 | 1214 | // Print the filesize. 1215 | echo $this->markup_filesize_location_indicator(); 1216 | 1217 | // Filter before legend; for any explanatory text. 1218 | echo apply_filters(PLUGIN_PREFIX.'_before_legend', ''); 1219 | 1220 | // Print the legend. 1221 | echo $this->markup_legend(); 1222 | 1223 | // Filter after legend; for any explanatory text. 1224 | echo apply_filters(PLUGIN_PREFIX.'_after_legend', ''); 1225 | 1226 | // If error log is empty, go no further; close wrapper and return. 1227 | if (empty($this->errors)) { 1228 | echo '
'; 1229 | return; 1230 | } 1231 | 1232 | // Print the error rows. 1233 | echo $this->markup_error_rows(); 1234 | 1235 | // Another jump-link, if the display grows long. 1236 | echo $this->markup_jump_link('footer'); 1237 | 1238 | // Print buttons to refresh and purge errors; for long pages. 1239 | if ($this->errors_displayed > 10) { 1240 | echo $this->markup_action_buttons('bottom'); 1241 | } 1242 | 1243 | // That's a wrap – thanks, everyone! 1244 | echo ''; 1245 | 1246 | } 1247 | 1248 | /** 1249 | * Filter footer text 1250 | * 1251 | * @author John Alarcon 1252 | * 1253 | * @since 1.0.0 1254 | * 1255 | * @param string $text The original footer text. 1256 | * 1257 | * @return void|string Branded footer text if in this plugin's admin. 1258 | */ 1259 | public function filter_footer_text($text) { 1260 | 1261 | // Are we on this post type's screen? If so, change the footer text. 1262 | if (strpos(get_current_screen()->base, PLUGIN_SHORT_SLUG)) { 1263 | $text = ''.PLUGIN_NAME.' '.PLUGIN_VERSION.' – by '.PLUGIN_AUTHOR.''; 1264 | } 1265 | 1266 | // Return the string. 1267 | return $text; 1268 | 1269 | } 1270 | 1271 | /** 1272 | * Plugin activation 1273 | * 1274 | * @author John Alarcon 1275 | * 1276 | * @since 1.0.0 1277 | */ 1278 | public function activate_plugin() { 1279 | 1280 | // No permission to activate plugins? Bail. 1281 | if (!current_user_can('activate_plugins')) { 1282 | return; 1283 | } 1284 | 1285 | // Initialize the options array. 1286 | $options = []; 1287 | 1288 | // Make sure the datetime variable is set. 1289 | $options['datetime'] = 1; 1290 | 1291 | // Iterate over error types; ensure they, too, are set. 1292 | foreach (array_keys($this->get_error_types()) as $type) { 1293 | $options[$type] = 1; 1294 | } 1295 | 1296 | // Set the sort order. 1297 | $options['reverse_sort'] = 0; 1298 | 1299 | // Update with defaults. 1300 | update_option(PLUGIN_PREFIX, $options); 1301 | 1302 | } 1303 | 1304 | /** 1305 | * Plugin deactivation 1306 | * 1307 | * @author John Alarcon 1308 | * 1309 | * @since 1.0.0 1310 | */ 1311 | public function deactivate_plugin() { 1312 | 1313 | // No permission to activate plugins? None to deactivate either. Bail. 1314 | if (!current_user_can('activate_plugins')) { 1315 | return; 1316 | } 1317 | 1318 | // Not that there was anything to do here anyway. :) 1319 | 1320 | } 1321 | 1322 | /** 1323 | * Plugin deletion 1324 | * 1325 | * @author John Alarcon 1326 | * 1327 | * @since 1.0.0 1328 | */ 1329 | public static function uninstall_plugin() { 1330 | 1331 | // No permission to delete plugins? Bail. 1332 | if (!current_user_can('delete_plugins')) { 1333 | return; 1334 | } 1335 | 1336 | // Delete options related to the plugin. 1337 | delete_option(PLUGIN_PREFIX); 1338 | 1339 | } 1340 | 1341 | } 1342 | 1343 | // Make awesome all the errors. 1344 | new PhpErrorLogViewer; -------------------------------------------------------------------------------- /images/banner-1544x500.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codecook-start/PHP-Log/31826bd8756206480d15b9ce2655c32bf1d305c3/images/banner-1544x500.png -------------------------------------------------------------------------------- /images/banner-772x250.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codecook-start/PHP-Log/31826bd8756206480d15b9ce2655c32bf1d305c3/images/banner-772x250.png -------------------------------------------------------------------------------- /images/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codecook-start/PHP-Log/31826bd8756206480d15b9ce2655c32bf1d305c3/images/icon-128.png -------------------------------------------------------------------------------- /images/icon-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codecook-start/PHP-Log/31826bd8756206480d15b9ce2655c32bf1d305c3/images/icon-256.png -------------------------------------------------------------------------------- /images/index.php: -------------------------------------------------------------------------------- 1 | '.gettype($data).''; 52 | 53 | // If file path or line number exist, tack on a separator. 54 | if ($file || $line) { 55 | $msg .= ': '; 56 | } 57 | 58 | /** 59 | * Validate and add file path, if any. Because Windows-based drives will 60 | * not pass validate_file() (due to the drive name and colon,) the first 61 | * block here will remove those parts, if present, before validating the 62 | * path. If the path is then valid, those parts will be added back to it 63 | * for display in the message. In this case, only the filename shows; to 64 | * view the full path, the filename can be hovered. 65 | */ 66 | if ($file) { 67 | // Default flag. 68 | $windows = false; 69 | // Handle Windows-based drive paths. 70 | if (substr($file, 1, 1) === ':') { 71 | $drive = substr($file, 0, 2); 72 | $file = substr($file, 2); 73 | $windows = true; 74 | } 75 | // If path is valid, add it to the message. 76 | if (validate_file($file) === 0) { 77 | if ($windows) { 78 | $file = $drive.$file; 79 | } 80 | $msg .= ''.esc_html(basename($file)).''; 81 | } 82 | } 83 | 84 | // Validate and add line number, if any. 85 | if ($line) { 86 | $msg .= sprintf( 87 | esc_html__(' at line %d', 'codepotent-php-error-log-viewer'), 88 | $line 89 | ); 90 | } 91 | 92 | // Done with first line, break to a new line. 93 | $msg .= '
'; 94 | 95 | // One last thing: convert true/false/null into displayalbe strings. 96 | if (is_bool($data) && !is_numeric($data)) { 97 | if ($data) { 98 | $data = 'true'; 99 | } else { 100 | $data = 'false'; 101 | } 102 | } else if (is_null($data)) { 103 | $data = 'null'; 104 | } 105 | 106 | // Convert $data into a preformatted string and add it to the message. 107 | $msg .= '
'.str_replace(["\r","\n"], '
', print_r($data, true)).'
'; 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 |