| @@ -37,8 +37,10 @@ | ||
| 37 | 37 | add_action( 'wp_ajax_sureforms_recommended_plugin_activate', [ $this, 'required_plugin_activate' ] ); |
| 38 | 38 | add_action( 'wp_ajax_sureforms_recommended_plugin_install', 'wp_ajax_install_plugin' ); |
| 39 | 39 | add_action( 'wp_ajax_sureforms_integration', [ $this, 'generate_data_for_suretriggers_integration' ] ); |
| 40 | 40 | add_action( 'wp_ajax_srfm_download_export', [ $this, 'download_export_file' ] ); |
| 41 | + add_action( 'wp_ajax_srfm_download_logs', [ $this, 'download_client_log' ] ); | |
| 42 | + add_action( 'wp_ajax_srfm_clear_logs', [ $this, 'clear_client_log' ] ); | |
| 41 | 43 | |
| 42 | 44 | add_filter( SRFM_SLUG . '_admin_filter', [ $this, 'localize_script_integration' ] ); |
| 43 | 45 | } |
| 44 | 46 | |
| @@ -183,9 +185,9 @@ | ||
| 183 | 185 | } |
| 184 | 186 | |
| 185 | 187 | // Translators: %s: Form ID. |
| 186 | 188 | $form_name = ! empty( $form->post_title ) ? $form->post_title : sprintf( __( 'SureForms id: %s', 'sureforms' ), $form_id ); |
| 187 | - $api_url = apply_filters( 'suretriggers_get_iframe_url', SRFM_SURETRIGGERS_INTEGRATION_BASE_URL ); | |
| 189 | + $api_url = apply_filters( 'suretriggers_get_iframe_url', SRFM_SURETRIGGERS_INTEGRATION_BASE_URL ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- SureTriggers' own filter; the name must match SureTriggers exactly to integrate. | |
| 188 | 190 | |
| 189 | 191 | // This is the format of data required by SureTriggers for adding iframe in target id. |
| 190 | 192 | $body = [ |
| 191 | 193 | 'client_id' => 'SureForms', |
| @@ -450,12 +452,98 @@ | ||
| 450 | 452 | ob_end_clean(); |
| 451 | 453 | } |
| 452 | 454 | |
| 453 | 455 | // Output the file. |
| 454 | - readfile( $filepath ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_readfile -- Need direct file output for download. | |
| 456 | + readfile( $filepath ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_readfile, WordPress.WP.AlternativeFunctions.file_system_operations_readfile -- Direct file output is required to stream the download. | |
| 455 | 457 | |
| 456 | 458 | // Clean up the temporary file. |
| 457 | - unlink( $filepath ); | |
| 459 | + wp_delete_file( $filepath ); | |
| 458 | 460 | |
| 459 | 461 | exit; |
| 460 | 462 | } |
| 463 | + /** | |
| 464 | + * Stream the client debug log to an administrator. | |
| 465 | + * | |
| 466 | + * Takes no filename parameter. There is exactly one log file and the server | |
| 467 | + * derives its path, which removes the path-traversal question entirely rather | |
| 468 | + * than guarding against it -- and keeps the unguessable file name, which is | |
| 469 | + * what actually protects the log on nginx, out of the page. | |
| 470 | + * | |
| 471 | + * @since 2.12.6 | |
| 472 | + * @return void | |
| 473 | + */ | |
| 474 | + public function download_client_log() { | |
| 475 | + $this->verify_log_request(); | |
| 476 | + | |
| 477 | + $path = Client_Logger::get_log_path( false ); | |
| 478 | + $has_log = '' !== $path && file_exists( $path ); | |
| 479 | + | |
| 480 | + // The buttons are always offered while logging is on, so downloading before | |
| 481 | + // anything has failed is a normal thing to do. Hand back an explanatory file | |
| 482 | + // rather than a wp_die() screen -- an empty log is the good outcome. | |
| 483 | + if ( ! $has_log ) { | |
| 484 | + header( 'Content-Type: text/plain; charset=utf-8' ); | |
| 485 | + header( 'X-Content-Type-Options: nosniff' ); | |
| 486 | + header( 'Content-Disposition: attachment; filename="sureforms-debug-log.txt"' ); | |
| 487 | + | |
| 488 | + if ( ob_get_level() ) { | |
| 489 | + ob_end_clean(); | |
| 490 | + } | |
| 491 | + | |
| 492 | + echo esc_html__( 'No form submission failures have been recorded.', 'sureforms' ); | |
| 493 | + exit; | |
| 494 | + } | |
| 495 | + | |
| 496 | + $size = filesize( $path ); | |
| 497 | + | |
| 498 | + header( 'Content-Type: text/plain; charset=utf-8' ); | |
| 499 | + header( 'X-Content-Type-Options: nosniff' ); | |
| 500 | + header( 'Content-Disposition: attachment; filename="sureforms-debug-log.txt"' ); | |
| 501 | + | |
| 502 | + if ( is_int( $size ) ) { | |
| 503 | + header( 'Content-Length: ' . $size ); | |
| 504 | + } | |
| 505 | + | |
| 506 | + header( 'Cache-Control: private, max-age=0, must-revalidate' ); | |
| 507 | + | |
| 508 | + if ( ob_get_level() ) { | |
| 509 | + ob_end_clean(); | |
| 510 | + } | |
| 511 | + | |
| 512 | + readfile( $path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_readfile, WordPress.WP.AlternativeFunctions.file_system_operations_readfile -- Direct file output is required to stream the download. | |
| 513 | + exit; | |
| 514 | + } | |
| 515 | + | |
| 516 | + /** | |
| 517 | + * Delete the client debug log. | |
| 518 | + * | |
| 519 | + * @since 2.12.6 | |
| 520 | + * @return void | |
| 521 | + */ | |
| 522 | + public function clear_client_log() { | |
| 523 | + $this->verify_log_request(); | |
| 524 | + | |
| 525 | + Client_Logger::clear(); | |
| 526 | + | |
| 527 | + wp_send_json_success(); | |
| 528 | + } | |
| 529 | + | |
| 530 | + /** | |
| 531 | + * Capability and nonce gate shared by both log actions. | |
| 532 | + * | |
| 533 | + * Capability first, ahead of the nonce, matching the ordering of the sibling | |
| 534 | + * handlers in this class. | |
| 535 | + * | |
| 536 | + * @since 2.12.6 | |
| 537 | + * @return void | |
| 538 | + */ | |
| 539 | + private function verify_log_request() { | |
| 540 | + if ( ! Helper::current_user_can() ) { | |
| 541 | + wp_die( esc_html__( 'You do not have permission to access this file.', 'sureforms' ) ); | |
| 542 | + } | |
| 543 | + | |
| 544 | + if ( ! isset( $_REQUEST['_wpnonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['_wpnonce'] ) ), 'srfm_client_logs' ) ) { | |
| 545 | + wp_die( esc_html__( 'Security check failed.', 'sureforms' ) ); | |
| 546 | + } | |
| 547 | + } | |
| 548 | + | |
| 461 | 549 | } |