\WP_REST_Server::READABLE, 'callback' => array( $this, 'get_debug_email' ), 'permission_callback' => $this->get_admin_permission_callback(), ), ); self::register_rest_endpoint( self::ENDPOINT_PATH, self::ENDPOINT_VERSION, array( 'methods' => \WP_REST_Server::CREATABLE, 'callback' => array( $this, 'send_debug_email' ), 'permission_callback' => $this->get_admin_permission_callback(), ), ); } /** * Handles the request to return the debug payload. * * @return \WP_REST_Response */ public function get_debug_email(): \WP_REST_Response { $current_user = wp_get_current_user(); return $this->return_api_response( \WP_Http::OK, 'Successfully retrieved debug data.', array( 'debugData' => $this->get_debug_data_with_keys(), 'submitterEmail' => ! empty( $current_user->user_email ) ? $current_user->user_email : null, ) ); } /** * Returns a standardized error response for support topic validation. * * @param int $status_code The HTTP status code. * @param string $message_key The translation key for the error. * * @return \WP_REST_Response */ private function get_support_topic_error_response( int $status_code, string $message_key ): \WP_REST_Response { return $this->return_api_response( $status_code, $message_key, array( 'field' => 'supportTopicUrl', ) ); } /** * Builds the ordered list of rows for the debug preview UI. * * @return array Array of { key, value } objects. */ private function get_debug_data_with_keys(): array { $payload = $this->get_debug_data(); return array( array( 'key' => 'ADMIN_TROUBLESHOOTING.DEBUG_MODAL_LABEL_SITE_NAME', 'value' => $payload['siteName'], ), array( 'key' => 'ADMIN_TROUBLESHOOTING.DEBUG_MODAL_LABEL_SITE_URL', 'value' => $payload['siteUrl'], ), array( 'key' => 'ADMIN_TROUBLESHOOTING.DEBUG_MODAL_LABEL_FR_VERSION', 'value' => $payload['forceRefreshVersion'], ), array( 'key' => 'ADMIN_TROUBLESHOOTING.DEBUG_MODAL_LABEL_SITE_VERSION', 'value' => $payload['siteVersion'], ), array( 'key' => 'ADMIN_TROUBLESHOOTING.DEBUG_MODAL_LABEL_REFRESH_INTERVAL', 'value' => sprintf( '%ss', $payload['refreshInterval'] ), ), array( 'key' => 'ADMIN_TROUBLESHOOTING.DEBUG_MODAL_LABEL_WP_VERSION', 'value' => $payload['wordPressVersion'], ), array( 'key' => 'ADMIN_TROUBLESHOOTING.DEBUG_MODAL_LABEL_PHP_VERSION', 'value' => $payload['phpVersion'], ), ...$this->get_scheduled_refresh_debug_rows( $payload['scheduledRefreshes'] ), array( 'key' => 'ADMIN_TROUBLESHOOTING.DEBUG_MODAL_LABEL_LAST_CRON_RUN', 'value' => $payload['lastCronRun'] ?? __( 'Never', 'force-refresh' ), ), array( 'key' => 'ADMIN_TROUBLESHOOTING.DEBUG_MODAL_LABEL_UPDATE_METHOD', 'value' => $payload['staticFilePollingEnabled'] ? __( 'Static file', 'force-refresh' ) : __( 'WordPress API', 'force-refresh' ), ), array( 'key' => 'ADMIN_TROUBLESHOOTING.DEBUG_MODAL_LABEL_DETECTED_CDN', 'value' => $payload['detectedCdn'] ?? __( 'None detected', 'force-refresh' ), ), ); } /** * Builds the debug payload from the current WordPress environment. * * @return array The debug payload. */ private function get_debug_data(): array { $plugin_data = ForceRefresh\get_force_refresh_plugin_data(); $scheduled_refreshes = Api_Handler_Admin_Schedule_Refresh_Site::get_scheduled_refreshes(); $last_cron_run = Api_Handler_Admin_Schedule_Refresh_Site::get_last_cron_run(); return array( 'siteUrl' => get_bloginfo( 'url' ), 'siteName' => get_bloginfo( 'name' ), 'wordPressVersion' => ForceRefresh\get_wordpress_version(), 'phpVersion' => phpversion(), 'forceRefreshVersion' => $plugin_data['Version'], 'siteVersion' => Versions_Storage_Service::get_site_version(), 'refreshInterval' => Options_Storage_Service::get_refresh_interval(), 'scheduledRefreshes' => $this->format_scheduled_refreshes( $scheduled_refreshes ), 'lastCronRun' => $this->format_timestamp_utc( $last_cron_run ), 'staticFilePollingEnabled' => Options_Storage_Service::get_use_static_file_polling(), 'detectedCdn' => Cdn_Detection_Service::get_detected_cdn(), ); } /** * Build the scheduled refresh rows for the debug modal payload. * * @param array $scheduled_refreshes The formatted scheduled refresh dates. * * @return array */ private function get_scheduled_refresh_debug_rows( array $scheduled_refreshes ): array { if ( empty( $scheduled_refreshes ) ) { return array( array( 'key' => 'ADMIN_TROUBLESHOOTING.DEBUG_MODAL_LABEL_SCHEDULED_REFRESHES', 'value' => __( 'None', 'force-refresh' ), ), ); } return array_map( fn( $date, $index ) => array( 'key' => 'ADMIN_TROUBLESHOOTING.DEBUG_MODAL_LABEL_SCHEDULED_REFRESH', 'index' => $index + 1, 'value' => $date, ), $scheduled_refreshes, array_keys( $scheduled_refreshes ) ); } /** * Format scheduled refresh timestamps for debug output. * * @param array $scheduled_refreshes The raw scheduled refresh data. * * @return array */ private function format_scheduled_refreshes( array $scheduled_refreshes ): array { return array_map( fn( $scheduled_refresh ) => $this->format_timestamp_utc( $scheduled_refresh['timestamp'] ), $scheduled_refreshes ); } /** * Format a Unix timestamp as a UTC string for debug output. * * @param int|null $timestamp The timestamp to format. * * @return string|null */ private function format_timestamp_utc( ?int $timestamp ): ?string { if ( empty( $timestamp ) ) { return null; } return gmdate( 'F j, Y \a\t g:i:s A', $timestamp ) . ' UTC'; } /** * Validates that the provided URL is a WordPress.org support topic URL. * * @param string $support_topic_url The user-provided support topic URL. * * @return bool Whether the URL shape is valid. */ private function is_valid_support_topic_url( string $support_topic_url ): bool { $parsed_url = wp_parse_url( $support_topic_url ); // Support topic has to be on the wordpress.org domain. if ( empty( $parsed_url['host'] ) || 'wordpress.org' !== strtolower( $parsed_url['host'] ) ) { return false; } // Support topic has to use http or https. if ( empty( $parsed_url['scheme'] ) || ! in_array( strtolower( $parsed_url['scheme'] ), array( 'http', 'https' ), true ) ) { return false; } // Support topic path has to exist. if ( empty( $parsed_url['path'] ) ) { return false; } // Support topics have to match the WordPress support topic patterns. return 1 === preg_match( '#^/support/topic/[^/]+/?$#', $parsed_url['path'] ); } /** * Checks whether the supplied WordPress.org support topic is unresolved. * * @param string $support_topic_url The validated topic URL. * * @return true|\WP_REST_Response True when unresolved, or an error response. */ private function validate_support_topic_is_unresolved( string $support_topic_url ) { $response = wp_remote_get( $support_topic_url, array( 'redirection' => 3, 'timeout' => 10, ) ); if ( is_wp_error( $response ) ) { return $this->get_support_topic_error_response( \WP_Http::BAD_GATEWAY, 'ADMIN_TROUBLESHOOTING.DEBUG_MODAL_SUPPORT_URL_UNAVAILABLE' ); } $status_code = wp_remote_retrieve_response_code( $response ); if ( \WP_Http::OK !== $status_code ) { return $this->get_support_topic_error_response( \WP_Http::BAD_REQUEST, 'ADMIN_TROUBLESHOOTING.DEBUG_MODAL_SUPPORT_URL_NOT_FOUND' ); } $response_body = wp_remote_retrieve_body( $response ); $normalized = strtolower( preg_replace( '/\s+/', ' ', \wp_strip_all_tags( $response_body ) ) ); if ( false !== strpos( $normalized, 'status: not resolved' ) ) { return true; } if ( false !== strpos( $normalized, 'status: resolved' ) ) { return $this->get_support_topic_error_response( \WP_Http::CONFLICT, 'ADMIN_TROUBLESHOOTING.DEBUG_MODAL_SUPPORT_URL_RESOLVED' ); } return $this->get_support_topic_error_response( \WP_Http::BAD_GATEWAY, 'ADMIN_TROUBLESHOOTING.DEBUG_MODAL_SUPPORT_URL_UNCONFIRMED' ); } /** * Formats the debug payload as a plain-text email body. * * @param array $payload The debug payload. * @param string $support_topic_url The support topic URL provided by the user. * * @return string The formatted email body. */ private function format_email_body( array $payload, string $support_topic_url ): string { return implode( "\n", array( 'A Force Refresh debug report was submitted.', '', sprintf( 'Support Topic URL: %s', $support_topic_url ), sprintf( 'Site Name: %s', $payload['siteName'] ), sprintf( 'Site URL: %s', $payload['siteUrl'] ), sprintf( 'Force Refresh Version: %s', $payload['forceRefreshVersion'] ), sprintf( 'Current Site Version: %s', $payload['siteVersion'] ), sprintf( 'Refresh Interval: %ss', $payload['refreshInterval'] ), sprintf( 'WordPress Version: %s', $payload['wordPressVersion'] ), sprintf( 'PHP Version: %s', $payload['phpVersion'] ), ...$this->get_scheduled_refresh_email_lines( $payload['scheduledRefreshes'] ), sprintf( 'Last Cron Run: %s', $payload['lastCronRun'] ?? 'Never' ), sprintf( 'Update Method: %s', $payload['staticFilePollingEnabled'] ? 'Static file' : 'WordPress API' ), sprintf( 'CDN: %s', $payload['detectedCdn'] ?? 'None detected' ), '', sprintf( 'Submitted: %s', gmdate( 'Y-m-d H:i:s T' ) ), ) ); } /** * Build the scheduled refresh lines for the plain-text debug email body. * * @param array $scheduled_refreshes The formatted scheduled refresh dates. * * @return array */ private function get_scheduled_refresh_email_lines( array $scheduled_refreshes ): array { if ( empty( $scheduled_refreshes ) ) { return array( 'Scheduled Refreshes: None' ); } return array_map( fn( $date, $index ) => sprintf( 'Scheduled Refresh %d: %s', $index + 1, $date ), $scheduled_refreshes, array_keys( $scheduled_refreshes ) ); } /** * Handles the request to send a debug report email. * * @param \WP_REST_Request $request The WordPress REST request. * * @return \WP_REST_Response */ public function send_debug_email( \WP_REST_Request $request ): \WP_REST_Response { $support_topic_url = esc_url_raw( trim( (string) $request->get_param( 'supportTopicUrl' ) ) ); if ( empty( $support_topic_url ) ) { return $this->get_support_topic_error_response( \WP_Http::BAD_REQUEST, 'ADMIN_TROUBLESHOOTING.DEBUG_MODAL_SUPPORT_URL_REQUIRED' ); } if ( ! $this->is_valid_support_topic_url( $support_topic_url ) ) { return $this->get_support_topic_error_response( \WP_Http::BAD_REQUEST, 'ADMIN_TROUBLESHOOTING.DEBUG_MODAL_SUPPORT_URL_INVALID' ); } // Only allow users to submit reports for support topics that are still unresolved. $support_topic_validation = $this->validate_support_topic_is_unresolved( $support_topic_url ); if ( true !== $support_topic_validation ) { return $support_topic_validation; } $payload = $this->get_debug_data(); $subject = sprintf( '[Force Refresh] Debug Report — %s', $payload['siteName'] ); $body = $this->format_email_body( $payload, $support_topic_url ); $current_user = wp_get_current_user(); $headers = $current_user->user_email ? array( sprintf( 'Cc: %s', $current_user->user_email ) ) : array(); $sent = wp_mail( self::RECIPIENT_EMAIL, $subject, $body, $headers ); if ( ! $sent ) { return $this->return_api_response( \WP_Http::INTERNAL_SERVER_ERROR, 'ADMIN_TROUBLESHOOTING.DEBUG_MODAL_SEND_FAILED' ); } return $this->return_api_response( \WP_Http::OK, 'Your debug report was sent successfully.' ); } /** * Method for getting the endpoint for this service. * * @return string The service endpoint. */ public static function get_rest_endpoint(): string { return self::get_formatted_rest_endpoint( self::ENDPOINT_PATH, self::ENDPOINT_VERSION ); } }