HEX
Server: Apache/2.4.68 (Debian)
System: Linux as-cs-widget-demo-us-central1 6.1.0-44-cloud-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.164-1 (2026-03-09) x86_64
User: root (0)
PHP: 8.2.32
Disabled: NONE
Upload Files
File: /var/www/html/wp-content/plugins/plugin-check/includes/Utilities/Results_Exporter.php
<?php
/**
 * Class WordPress\Plugin_Check\Utilities\Results_Exporter
 *
 * @package plugin-check
 */

namespace WordPress\Plugin_Check\Utilities;

use InvalidArgumentException;

/**
 * Utility helpers to normalise and export check results.
 *
 * @since 1.8.0
 */
final class Results_Exporter {

	/**
	 * CSV export format slug.
	 *
	 * @since 1.8.0
	 * @var string
	 */
	public const FORMAT_CSV = 'csv';

	/**
	 * JSON export format slug.
	 *
	 * @since 1.8.0
	 * @var string
	 */
	public const FORMAT_JSON = 'json';

	/**
	 * Markdown export format slug.
	 *
	 * @since 1.8.0
	 * @var string
	 */
	public const FORMAT_MARKDOWN = 'markdown';

	/**
	 * Normalises the errors and warnings arrays into grouped results by file.
	 *
	 * @since 1.8.0
	 *
	 * @param array $errors   Check errors grouped by file, line and column.
	 * @param array $warnings Check warnings grouped by file, line and column.
	 * @return array Results grouped by file.
	 */
	public static function get_grouped_results( array $errors, array $warnings ) {
		$grouped            = array();
		$remaining_warnings = $warnings;

		foreach ( $errors as $file => $file_errors ) {
			$file_warnings = array();
			if ( isset( $remaining_warnings[ $file ] ) ) {
				$file_warnings = $remaining_warnings[ $file ];
				unset( $remaining_warnings[ $file ] );
			}

			$grouped[ $file ] = self::flatten_file_results( $file_errors, $file_warnings );
		}

		foreach ( $remaining_warnings as $file => $file_warnings ) {
			$grouped[ $file ] = self::flatten_file_results( array(), $file_warnings );
		}

		return $grouped;
	}

	/**
	 * Returns a flat array of all results including the file name per item.
	 *
	 * @since 1.8.0
	 *
	 * @param array $grouped_results Results grouped by file.
	 * @return array Flat list of results.
	 */
	public static function get_flat_results( array $grouped_results ) {
		$flat_results = array();

		foreach ( $grouped_results as $file => $items ) {
			foreach ( $items as $item ) {
				$flat_results[] = array_merge(
					$item,
					array(
						'file' => $file,
					)
				);
			}
		}

		return $flat_results;
	}

	/**
	 * Creates an export payload for the requested format.
	 *
	 * @since 1.8.0
	 *
	 * @param array  $errors   Check errors grouped by file, line and column.
	 * @param array  $warnings Check warnings grouped by file, line and column.
	 * @param string $format   Target export format.
	 * @param array  $args     Additional arguments.
	 * @return array Export payload.
	 *
	 * @throws InvalidArgumentException When the format is invalid or there are no results.
	 */
	public static function export( array $errors, array $warnings, $format, array $args = array() ) {
		$format = strtolower( (string) $format );

		if ( ! in_array( $format, array( self::FORMAT_CSV, self::FORMAT_JSON, self::FORMAT_MARKDOWN ), true ) ) {
			throw new InvalidArgumentException( __( 'Unsupported export format.', 'plugin-check' ) );
		}

		$grouped = self::get_grouped_results( $errors, $warnings );
		$flat    = self::get_flat_results( $grouped );

		$timestamp = isset( $args['timestamp'] ) ? $args['timestamp'] : current_time( 'Ymd-His' );
		$plugin    = isset( $args['plugin'] ) ? $args['plugin'] : 'plugin-check-report';
		$slug      = isset( $args['slug'] ) ? $args['slug'] : $plugin;
		$filename  = self::build_filename( $slug, $timestamp, $format );

		switch ( $format ) {
			case self::FORMAT_JSON:
				$content   = self::to_json( $grouped, $args );
				$mime_type = 'application/json';
				break;
			case self::FORMAT_MARKDOWN:
				$content   = self::to_markdown( $grouped, $args );
				$mime_type = 'text/markdown';
				break;
			case self::FORMAT_CSV:
			default:
				$content   = self::to_csv( $flat );
				$mime_type = 'text/csv';
				break;
		}

		return array(
			'content'   => $content,
			'filename'  => $filename,
			'mime_type' => $mime_type,
		);
	}

	/**
	 * Builds a filename for the export.
	 *
	 * @since 1.8.0
	 *
	 * @param string $slug      Plugin slug.
	 * @param string $timestamp Export timestamp.
	 * @param string $format    Target format.
	 * @return string Export filename.
	 */
	private static function build_filename( $slug, $timestamp, $format ) {
		$normalized_slug = sanitize_title( $slug );
		if ( empty( $normalized_slug ) ) {
			$normalized_slug = 'plugin-check-report';
		}

		$extension = $format;
		if ( self::FORMAT_MARKDOWN === $format ) {
			$extension = 'md';
		}

		return sprintf( '%1$s-%2$s.%3$s', $normalized_slug, $timestamp, $extension );
	}

	/**
	 * Generates JSON content for the results.
	 *
	 * @since 1.8.0
	 *
	 * @param array $grouped_results Grouped results indexed by file name.
	 * @param array $args            Additional arguments.
	 * @return string JSON export content.
	 */
	private static function to_json( array $grouped_results, array $args ) {
		$payload = array(
			'generated_at' => isset( $args['timestamp_human'] ) ? $args['timestamp_human'] : current_time( 'mysql' ),
			'plugin'       => isset( $args['plugin'] ) ? $args['plugin'] : '',
			'results'      => $grouped_results,
		);

		return wp_json_encode( $payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES );
	}

	/**
	 * Generates CSV content for the results.
	 *
	 * @since 1.8.0
	 *
	 * @param array $flat_results Flat list of results including the file name.
	 * @return string CSV export content.
	 */
	private static function to_csv( array $flat_results ) {
		$handle = fopen( 'php://temp', 'r+' );

		fputcsv(
			$handle,
			array( 'file', 'line', 'column', 'type', 'code', 'message', 'docs', 'severity', 'link' ),
			',',
			'"',
			'\\'
		);

		foreach ( $flat_results as $row ) {
			fputcsv(
				$handle,
				array(
					$row['file'],
					sprintf( '%d', $row['line'] ),
					sprintf( '%d', $row['column'] ),
					$row['type'],
					$row['code'],
					$row['message'],
					isset( $row['docs'] ) ? $row['docs'] : '',
					isset( $row['severity'] ) ? $row['severity'] : '',
					isset( $row['link'] ) ? $row['link'] : '',
				),
				',',
				'"',
				'\\'
			);
		}

		rewind( $handle );
		$content = stream_get_contents( $handle );
		fclose( $handle );

		return (string) $content;
	}

	/**
	 * Generates Markdown content for the results.
	 *
	 * @since 1.8.0
	 *
	 * @param array $grouped_results Grouped results indexed by file name.
	 * @param array $args            Additional arguments.
	 * @return string Markdown export content.
	 */
	private static function to_markdown( array $grouped_results, array $args ) {
		$plugin_name    = isset( $args['plugin'] ) ? $args['plugin'] : '';
		$generated_at   = isset( $args['timestamp_human'] ) ? $args['timestamp_human'] : current_time( 'mysql' );
		$markdown_lines = array(
			'# Plugin Check Report',
			'',
			'**Plugin:** ' . ( $plugin_name ? $plugin_name : __( 'Unknown plugin', 'plugin-check' ) ),
			'**Generated at:** ' . $generated_at,
			'',
		);

		$markdown_lines[] = '';
		foreach ( $grouped_results as $file => $items ) {
			$markdown_lines[] = '## `' . $file . '`';
			$markdown_lines[] = '';
			$markdown_lines[] = '| Line | Column | Type | Code | Message | Docs |';
			$markdown_lines[] = '| --- | --- | --- | --- | --- | --- |';

			foreach ( $items as $item ) {
				$markdown_lines[] = sprintf(
					'| %1$d | %2$d | %3$s | %4$s | %5$s | %6$s |',
					$item['line'],
					$item['column'],
					sanitize_text_field( $item['type'] ),
					sanitize_text_field( $item['code'] ),
					self::escape_markdown( $item['message'] ),
					isset( $item['docs'] ) && $item['docs'] ? '[' . __( 'Docs', 'plugin-check' ) . '](' . esc_url_raw( $item['docs'] ) . ')' : ''
				);
			}

			$markdown_lines[] = '';
		}

		return implode( "\n", $markdown_lines );
	}

	/**
	 * Escapes Markdown table cells.
	 *
	 * @since 1.8.0
	 *
	 * @param string $value Cell value.
	 * @return string Escaped value.
	 */
	private static function escape_markdown( $value ) {
		$value = wp_strip_all_tags( (string) $value );
		$value = preg_replace( '/\s+/', ' ', $value );
		$value = str_replace( array( '|', '`' ), array( '\\|', '\\`' ), $value );

		return trim( $value );
	}

	/**
	 * Flattens errors and warnings for a specific file into a list of rows.
	 *
	 * @since 1.8.0
	 *
	 * @param array $file_errors   Errors from a Check_Result, for a specific file.
	 * @param array $file_warnings Warnings from a Check_Result, for a specific file.
	 * @return array Combined file results.
	 */
	public static function flatten_file_results( array $file_errors, array $file_warnings ) {
		$file_results = array_merge(
			self::flatten_collection( $file_errors, 'ERROR' ),
			self::flatten_collection( $file_warnings, 'WARNING' )
		);

		usort(
			$file_results,
			static function ( $a, $b ) {
				if ( $a['line'] < $b['line'] ) {
					return -1;
				}
				if ( $a['line'] > $b['line'] ) {
					return 1;
				}
				if ( $a['column'] < $b['column'] ) {
					return -1;
				}
				if ( $a['column'] > $b['column'] ) {
					return 1;
				}
				return 0;
			}
		);

		return $file_results;
	}

	/**
	 * Flattens a collection of check results for a single message type.
	 *
	 * @since 1.8.0
	 *
	 * @param array  $collection Collection grouped by line and column.
	 * @param string $type       Message type, either ERROR or WARNING.
	 * @return array Flattened collection.
	 */
	private static function flatten_collection( array $collection, $type ) {
		$flattened = array();

		foreach ( $collection as $line => $line_entries ) {
			foreach ( $line_entries as $column => $column_entries ) {
				foreach ( $column_entries as $entry ) {
					$flattened[] = self::normalize_entry( $entry, $type, $line, $column );
				}
			}
		}

		return $flattened;
	}

	/**
	 * Normalizes a single check result entry into the export structure.
	 *
	 * @since 1.8.0
	 *
	 * @param array  $entry  Entry data.
	 * @param string $type   Message type.
	 * @param int    $line   Line number.
	 * @param int    $column Column number.
	 * @return array Normalized entry.
	 */
	private static function normalize_entry( array $entry, $type, $line, $column ) {
		$entry['message'] = str_replace(
			array( '<br>', '<strong>', '</strong>', '<code>', '</code>' ),
			array( ' ', '', '', '`', '`' ),
			$entry['message']
		);

		if ( 'ERROR' === $type ) {
			$entry['message'] = html_entity_decode( $entry['message'], ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401 );
		}

		return array_merge(
			$entry,
			array(
				'type'   => $type,
				'line'   => (int) $line,
				'column' => (int) $column,
			)
		);
	}
}