| 1 |
<?php |
| 2 |
/** |
| 3 |
* Logging class. |
| 4 |
* |
| 5 |
* @package ContentControl\Plugin |
| 6 |
*/ |
| 7 |
|
| 8 |
namespace ContentControl\Plugin; |
| 9 |
|
| 10 |
/** |
| 11 |
* Logging class. |
| 12 |
*/ |
| 13 |
class Logging { |
| 14 |
|
| 15 |
/** |
| 16 |
* Log file prefix. |
| 17 |
*/ |
| 18 |
const LOG_FILE_PREFIX = 'content-control-'; |
| 19 |
|
| 20 |
/** |
| 21 |
* Whether the log file is writable. |
| 22 |
* |
| 23 |
* @var bool|null |
| 24 |
*/ |
| 25 |
private $is_writable; |
| 26 |
|
| 27 |
/** |
| 28 |
* Log file name. |
| 29 |
* |
| 30 |
* @var string |
| 31 |
*/ |
| 32 |
private $filename = ''; |
| 33 |
|
| 34 |
/** |
| 35 |
* Log file path. |
| 36 |
* |
| 37 |
* @var string |
| 38 |
*/ |
| 39 |
private $file = ''; |
| 40 |
|
| 41 |
/** |
| 42 |
* File system API. |
| 43 |
* |
| 44 |
* @var \WP_Filesystem_Base|null |
| 45 |
*/ |
| 46 |
private $fs; |
| 47 |
|
| 48 |
/** |
| 49 |
* Log file content. |
| 50 |
* |
| 51 |
* @var string|null |
| 52 |
*/ |
| 53 |
private $content; |
| 54 |
|
| 55 |
/** |
| 56 |
* Initialize logging. |
| 57 |
*/ |
| 58 |
public function __construct() { |
| 59 |
$this->init(); |
| 60 |
|
| 61 |
$this->register_hooks(); |
| 62 |
} |
| 63 |
|
| 64 |
/** |
| 65 |
* Register hooks. |
| 66 |
* |
| 67 |
* @return void |
| 68 |
*/ |
| 69 |
public function register_hooks() { |
| 70 |
// On shutdown, save the log file. |
| 71 |
add_action( 'shutdown', [ $this, 'save_logs' ] ); |
| 72 |
} |
| 73 |
|
| 74 |
/** |
| 75 |
* Gets the Uploads directory |
| 76 |
* |
| 77 |
* @return bool|array{path: string, url: string, subdir: string, basedir: string, baseurl: string, error: string|false} An associated array with baseurl and basedir or false on failure |
| 78 |
*/ |
| 79 |
public function get_upload_dir() { |
| 80 |
// Used if you only need to fetch data, not create missing folders. |
| 81 |
$wp_upload_dir = wp_get_upload_dir(); |
| 82 |
|
| 83 |
// phpcs:ignore Squiz.PHP.CommentedOutCode.Found |
| 84 |
// $wp_upload_dir = wp_upload_dir(); // Disable this on IS_WPCOM if used. |
| 85 |
|
| 86 |
if ( isset( $wp_upload_dir['error'] ) && false !== $wp_upload_dir['error'] ) { |
| 87 |
return false; |
| 88 |
} else { |
| 89 |
return $wp_upload_dir; |
| 90 |
} |
| 91 |
} |
| 92 |
|
| 93 |
/** |
| 94 |
* Gets the uploads directory URL |
| 95 |
* |
| 96 |
* @param string $path A path to append to end of upload directory URL. |
| 97 |
* @return bool|string The uploads directory URL or false on failure |
| 98 |
*/ |
| 99 |
public function get_upload_dir_url( $path = '' ) { |
| 100 |
$upload_dir = $this->get_upload_dir(); |
| 101 |
if ( false !== $upload_dir && isset( $upload_dir['baseurl'] ) ) { |
| 102 |
$url = preg_replace( '/^https?:/', '', $upload_dir['baseurl'] ); |
| 103 |
if ( null === $url ) { |
| 104 |
return false; |
| 105 |
} |
| 106 |
if ( ! empty( $path ) ) { |
| 107 |
$url = trailingslashit( $url ) . $path; |
| 108 |
} |
| 109 |
return $url; |
| 110 |
} else { |
| 111 |
return false; |
| 112 |
} |
| 113 |
} |
| 114 |
|
| 115 |
/** |
| 116 |
* Chek if logging is enabled. |
| 117 |
* |
| 118 |
* @return bool |
| 119 |
*/ |
| 120 |
public function enabled() { |
| 121 |
$disabled = defined( '\CONTENT_CONTROL_DISABLE_LOGGING' ) && true === \CONTENT_CONTROL_DISABLE_LOGGING; |
| 122 |
|
| 123 |
return ! $disabled && $this->is_writable(); |
| 124 |
} |
| 125 |
|
| 126 |
/** |
| 127 |
* Get working WP Filesystem instance |
| 128 |
* |
| 129 |
* @return \WP_Filesystem_Base|false |
| 130 |
*/ |
| 131 |
public function fs() { |
| 132 |
if ( isset( $this->fs ) ) { |
| 133 |
return $this->fs; |
| 134 |
} |
| 135 |
|
| 136 |
global $wp_filesystem; |
| 137 |
|
| 138 |
require_once ABSPATH . 'wp-admin/includes/file.php'; |
| 139 |
|
| 140 |
// If for some reason the include doesn't work as expected just return false. |
| 141 |
if ( ! function_exists( 'WP_Filesystem' ) ) { |
| 142 |
return false; |
| 143 |
} |
| 144 |
|
| 145 |
$writable = WP_Filesystem( false, '', true ); |
| 146 |
|
| 147 |
// We consider the directory as writable if it uses the direct transport, |
| 148 |
// otherwise credentials would be needed. |
| 149 |
$this->fs = ( $writable && 'direct' === $wp_filesystem->method ) ? $wp_filesystem : false; |
| 150 |
|
| 151 |
return $this->fs; |
| 152 |
} |
| 153 |
|
| 154 |
/** |
| 155 |
* Check if the log file is writable. |
| 156 |
* |
| 157 |
* @return boolean |
| 158 |
*/ |
| 159 |
public function is_writable() { |
| 160 |
if ( isset( $this->is_writable ) ) { |
| 161 |
return $this->is_writable; |
| 162 |
} |
| 163 |
|
| 164 |
$file_system = $this->fs(); |
| 165 |
|
| 166 |
if ( false === $file_system ) { |
| 167 |
$this->is_writable = false; |
| 168 |
return $this->is_writable; |
| 169 |
} |
| 170 |
|
| 171 |
$this->is_writable = 'direct' === $file_system->method; |
| 172 |
|
| 173 |
$upload_dir = $this->get_upload_dir(); |
| 174 |
|
| 175 |
if ( ! $file_system->is_writable( $upload_dir['basedir'] ) ) { |
| 176 |
$this->is_writable = false; |
| 177 |
} |
| 178 |
|
| 179 |
return $this->is_writable; |
| 180 |
} |
| 181 |
|
| 182 |
/** |
| 183 |
* Get things started |
| 184 |
* |
| 185 |
* @return void |
| 186 |
*/ |
| 187 |
public function init() { |
| 188 |
$upload_dir = $this->get_upload_dir(); |
| 189 |
$file_system = $this->fs(); |
| 190 |
|
| 191 |
if ( false === $upload_dir || false === $file_system ) { |
| 192 |
return; |
| 193 |
} |
| 194 |
|
| 195 |
$file_token = \get_option( 'content_control_debug_log_token' ); |
| 196 |
if ( false === $file_token ) { |
| 197 |
$file_token = uniqid( (string) wp_rand(), true ); |
| 198 |
\update_option( 'content_control_debug_log_token', $file_token ); |
| 199 |
} |
| 200 |
|
| 201 |
$this->filename = self::LOG_FILE_PREFIX . "debug-{$file_token}.log"; // ex. content-control-debug-5c2f6a9b9b5a3.log. |
| 202 |
$this->file = trailingslashit( $upload_dir['basedir'] ) . $this->filename; |
| 203 |
|
| 204 |
if ( ! $file_system->exists( $this->file ) ) { |
| 205 |
$this->setup_new_log(); |
| 206 |
} else { |
| 207 |
$this->content = $this->get_file( $this->file ); |
| 208 |
} |
| 209 |
|
| 210 |
// Truncate long log files. |
| 211 |
if ( $file_system->exists( $this->file ) && $file_system->size( $this->file ) >= 1048576 ) { |
| 212 |
$this->truncate_log(); |
| 213 |
} |
| 214 |
} |
| 215 |
|
| 216 |
/** |
| 217 |
* Get the log file path. |
| 218 |
* |
| 219 |
* @return string |
| 220 |
*/ |
| 221 |
public function get_file_path() { |
| 222 |
return $this->file; |
| 223 |
} |
| 224 |
|
| 225 |
/** |
| 226 |
* Retrieves the url to the file |
| 227 |
* |
| 228 |
* @return string|bool The url to the file or false on failure |
| 229 |
*/ |
| 230 |
public function get_file_url() { |
| 231 |
if ( ! $this->enabled() ) { |
| 232 |
return false; |
| 233 |
} |
| 234 |
|
| 235 |
return $this->get_upload_dir_url( $this->filename ); |
| 236 |
} |
| 237 |
|
| 238 |
/** |
| 239 |
* Retrieve the log data |
| 240 |
* |
| 241 |
* @return false|string |
| 242 |
*/ |
| 243 |
public function get_log() { |
| 244 |
return $this->get_log_content(); |
| 245 |
} |
| 246 |
|
| 247 |
/** |
| 248 |
* Delete the log file and token. |
| 249 |
* |
| 250 |
* @return void |
| 251 |
*/ |
| 252 |
public function delete_logs() { |
| 253 |
$file_system = $this->fs(); |
| 254 |
|
| 255 |
if ( false === $file_system ) { |
| 256 |
return; |
| 257 |
} |
| 258 |
|
| 259 |
$file_system->delete( $this->file ); |
| 260 |
\delete_option( 'content_control_debug_log_token' ); |
| 261 |
} |
| 262 |
|
| 263 |
/** |
| 264 |
* Log message to file |
| 265 |
* |
| 266 |
* @param string $message The message to log. |
| 267 |
* |
| 268 |
* @return void |
| 269 |
*/ |
| 270 |
public function log( $message = '' ) { |
| 271 |
$this->write_to_log( wp_date( 'Y-n-d H:i:s' ) . ' - ' . $message ); |
| 272 |
} |
| 273 |
|
| 274 |
/** |
| 275 |
* Log unique message to file. |
| 276 |
* |
| 277 |
* @param string $message The unique message to log. |
| 278 |
* |
| 279 |
* @return void |
| 280 |
*/ |
| 281 |
public function log_unique( $message = '' ) { |
| 282 |
$contents = $this->get_log_content(); |
| 283 |
|
| 284 |
if ( strpos( $contents, $message ) !== false ) { |
| 285 |
return; |
| 286 |
} |
| 287 |
|
| 288 |
$this->log( $message ); |
| 289 |
} |
| 290 |
|
| 291 |
/** |
| 292 |
* Get the log file contents. |
| 293 |
* |
| 294 |
* @return false|string |
| 295 |
*/ |
| 296 |
public function get_log_content() { |
| 297 |
if ( ! isset( $this->content ) ) { |
| 298 |
$this->content = $this->get_file(); |
| 299 |
} |
| 300 |
|
| 301 |
return $this->content; |
| 302 |
} |
| 303 |
|
| 304 |
/** |
| 305 |
* Set the log file contents in memory. |
| 306 |
* |
| 307 |
* @param mixed $content The content to set. |
| 308 |
* @param bool $save Whether to save the content to the file immediately. |
| 309 |
* @return void |
| 310 |
*/ |
| 311 |
private function set_log_content( $content, $save = false ) { |
| 312 |
$this->content = $content; |
| 313 |
|
| 314 |
if ( $save ) { |
| 315 |
$this->save_logs(); |
| 316 |
} |
| 317 |
} |
| 318 |
|
| 319 |
/** |
| 320 |
* Retrieve the contents of a file. |
| 321 |
* |
| 322 |
* @param string|boolean $file File to get contents of. |
| 323 |
* |
| 324 |
* @return false|string |
| 325 |
*/ |
| 326 |
protected function get_file( $file = false ) { |
| 327 |
$file = $file ? $file : $this->file; |
| 328 |
|
| 329 |
$file_system = $this->fs(); |
| 330 |
|
| 331 |
if ( false === $file_system || ! $this->enabled() ) { |
| 332 |
return ''; |
| 333 |
} |
| 334 |
|
| 335 |
$content = ''; |
| 336 |
|
| 337 |
if ( $file_system->exists( $file ) ) { |
| 338 |
$content = $file_system->get_contents( $file ); |
| 339 |
} |
| 340 |
|
| 341 |
return $content; |
| 342 |
} |
| 343 |
|
| 344 |
/** |
| 345 |
* Write the log message |
| 346 |
* |
| 347 |
* @param string $message The message to write. |
| 348 |
* |
| 349 |
* @return void |
| 350 |
*/ |
| 351 |
protected function write_to_log( $message = '' ) { |
| 352 |
if ( ! $this->enabled() ) { |
| 353 |
return; |
| 354 |
} |
| 355 |
|
| 356 |
$contents = $this->get_log_content(); |
| 357 |
|
| 358 |
// If it doesn't end with a new line, add one. \r\n length is 2. |
| 359 |
if ( substr( $contents, -2 ) !== "\r\n" ) { |
| 360 |
$contents .= "\r\n"; |
| 361 |
} |
| 362 |
|
| 363 |
$this->set_log_content( $contents . $message ); |
| 364 |
} |
| 365 |
|
| 366 |
/** |
| 367 |
* Save the current contents to file. |
| 368 |
* |
| 369 |
* @return void |
| 370 |
*/ |
| 371 |
public function save_logs() { |
| 372 |
$file_system = $this->fs(); |
| 373 |
|
| 374 |
if ( false === $file_system || ! $this->enabled() ) { |
| 375 |
return; |
| 376 |
} |
| 377 |
|
| 378 |
$file_system->put_contents( $this->file, $this->content, FS_CHMOD_FILE ); |
| 379 |
} |
| 380 |
|
| 381 |
/** |
| 382 |
* Get a line count. |
| 383 |
* |
| 384 |
* @return int |
| 385 |
*/ |
| 386 |
public function count_lines() { |
| 387 |
$file = $this->get_log_content(); |
| 388 |
$lines = explode( "\r\n", $file ); |
| 389 |
|
| 390 |
return count( $lines ); |
| 391 |
} |
| 392 |
|
| 393 |
/** |
| 394 |
* Truncates a log file to maximum of 250 lines. |
| 395 |
* |
| 396 |
* @return void |
| 397 |
*/ |
| 398 |
public function truncate_log() { |
| 399 |
$content = $this->get_log_content(); |
| 400 |
$lines = explode( "\r\n", $content ); |
| 401 |
$lines = array_slice( $lines, 0, 250 ); // 50 is how many lines you want to keep |
| 402 |
$truncated_content = implode( "\r\n", $lines ); |
| 403 |
$this->set_log_content( $truncated_content, true ); |
| 404 |
} |
| 405 |
|
| 406 |
/** |
| 407 |
* Set up a new log file. |
| 408 |
* |
| 409 |
* @return void |
| 410 |
*/ |
| 411 |
public function setup_new_log() { |
| 412 |
$this->set_log_content( "Content Control Debug Logs:\r\n" . wp_date( 'Y-n-d H:i:s' ) . " - Log file initialized\r\n", true ); |
| 413 |
} |
| 414 |
|
| 415 |
/** |
| 416 |
* Delete the log file. |
| 417 |
* |
| 418 |
* @return void |
| 419 |
*/ |
| 420 |
public function clear_log() { |
| 421 |
$file_system = $this->fs(); |
| 422 |
|
| 423 |
if ( false === $file_system ) { |
| 424 |
return; |
| 425 |
} |
| 426 |
|
| 427 |
// Delete the file. |
| 428 |
// phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged |
| 429 |
@$file_system->delete( $this->file ); |
| 430 |
|
| 431 |
if ( $this->enabled() ) { |
| 432 |
$this->setup_new_log(); |
| 433 |
} |
| 434 |
} |
| 435 |
|
| 436 |
/** |
| 437 |
* Log a deprecated notice. |
| 438 |
* |
| 439 |
* @param string $func_name Function name. |
| 440 |
* @param string $version Versoin deprecated. |
| 441 |
* @param string $replacement Replacement function (optional). |
| 442 |
* |
| 443 |
* @return void |
| 444 |
*/ |
| 445 |
public function log_deprecated_notice( $func_name, $version, $replacement = null ) { |
| 446 |
if ( ! is_null( $replacement ) ) { |
| 447 |
$notice = sprintf( '%1$s is <strong>deprecated</strong> since version %2$s! Use %3$s instead.', $func_name, $version, $replacement ); |
| 448 |
} else { |
| 449 |
$notice = sprintf( '%1$s is <strong>deprecated</strong> since version %2$s with no alternative available.', $func_name, $version ); |
| 450 |
} |
| 451 |
|
| 452 |
$this->log_unique( $notice ); |
| 453 |
} |
| 454 |
} |
| 455 |
|