Skip to content

Commit 523f062

Browse files
davidperezgardavidperezgarLuc45ernilambarfrantorres
authored
Merge pull request #1271 from WordPress/1270-add-ctrf-export-format
Add CTRF export format for reports (admin + WP-CLI) Co-authored-by: davidperezgar <davidperez@git.wordpress.org> Co-authored-by: Luc45 <lucasbustamante@git.wordpress.org> Co-authored-by: ernilambar <nilambar@git.wordpress.org> Co-authored-by: frantorres <frantorres@git.wordpress.org>
2 parents abba5c0 + e31a148 commit 523f062

7 files changed

Lines changed: 293 additions & 3 deletions

File tree

assets/js/plugin-check-admin.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,10 @@
247247
format: 'json',
248248
label: defaultString( 'exportJson' ),
249249
},
250+
{
251+
format: 'ctrf',
252+
label: defaultString( 'exportCtrf' ),
253+
},
250254
{
251255
format: 'markdown',
252256
label: defaultString( 'exportMarkdown' ),

docs/CLI.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,18 @@ Applies after evaluating `--checks`.
2020
: Ignore error codes provided as an argument in comma-separated values.
2121
2222
[--format=<format>]
23-
: Format to display the results. Options are table, csv, json, strict-table, strict-csv, and strict-json. The default will be a table.
23+
: Format to display the results. Options are table, csv, json, ctrf, strict-table, strict-csv, strict-json, and strict-ctrf. The default will be a table.
2424
---
2525
default: table
2626
options:
2727
- table
2828
- csv
2929
- json
30+
- ctrf
3031
- strict-table
3132
- strict-csv
3233
- strict-json
34+
- strict-ctrf
3335
---
3436
3537
[--categories]
@@ -86,6 +88,7 @@ options:
8688
wp plugin check akismet
8789
wp plugin check akismet --checks=late_escaping
8890
wp plugin check akismet --format=json
91+
wp plugin check akismet --format=ctrf
8992
wp plugin check akismet --mode=update
9093
```
9194

includes/Admin/Admin_Page.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,7 @@ public function enqueue_scripts() {
207207
'strings' => array(
208208
'exportCsv' => __( 'Export CSV', 'plugin-check' ),
209209
'exportJson' => __( 'Export JSON', 'plugin-check' ),
210+
'exportCtrf' => __( 'Export CTRF', 'plugin-check' ),
210211
'exportMarkdown' => __( 'Export Markdown', 'plugin-check' ),
211212
'exporting' => __( 'Preparing export…', 'plugin-check' ),
212213
'exportError' => __( 'Export failed.', 'plugin-check' ),

includes/CLI/Plugin_Check_Command.php

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,11 @@ final class Plugin_Check_Command {
4343
'table',
4444
'csv',
4545
'json',
46+
'ctrf',
4647
'strict-table',
4748
'strict-csv',
4849
'strict-json',
50+
'strict-ctrf',
4951
);
5052

5153
/**
@@ -78,16 +80,18 @@ public function __construct( Plugin_Context $plugin_context ) {
7880
* : Ignore error codes provided as an argument in comma-separated values.
7981
*
8082
* [--format=<format>]
81-
* : Format to display the results. Options are table, csv, json, strict-table, strict-csv, and strict-json. The default will be a table.
83+
* : Format to display the results. Options are table, csv, json, ctrf, strict-table, strict-csv, strict-json, and strict-ctrf. The default will be a table.
8284
* ---
8385
* default: table
8486
* options:
8587
* - table
8688
* - csv
8789
* - json
90+
* - ctrf
8891
* - strict-table
8992
* - strict-csv
9093
* - strict-json
94+
* - strict-ctrf
9195
* ---
9296
*
9397
* [--categories]
@@ -324,6 +328,19 @@ static function ( $dirs ) use ( $excluded_files ) {
324328
}
325329
}
326330

331+
// Handle CTRF formats.
332+
if ( Results_Exporter::FORMAT_CTRF === $options['format'] || 'strict-' . Results_Exporter::FORMAT_CTRF === $options['format'] ) {
333+
$ctrf_report = Results_Exporter::to_ctrf_json(
334+
$all_results,
335+
array(
336+
'timestamp_iso' => gmdate( 'c' ),
337+
)
338+
);
339+
340+
WP_CLI::line( $ctrf_report );
341+
return;
342+
}
343+
327344
// Handle strict-* formats.
328345
if ( str_starts_with( $options['format'], 'strict-' ) ) {
329346
$base_format = substr( $options['format'], 7 );

includes/Utilities/Results_Exporter.php

Lines changed: 142 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,14 @@ final class Results_Exporter {
4040
*/
4141
public const FORMAT_MARKDOWN = 'markdown';
4242

43+
/**
44+
* CTRF export format slug.
45+
*
46+
* @since 2.0.0
47+
* @var string
48+
*/
49+
public const FORMAT_CTRF = 'ctrf';
50+
4351
/**
4452
* Normalises the errors and warnings arrays into grouped results by file.
4553
*
@@ -111,7 +119,7 @@ public static function get_flat_results( array $grouped_results ) {
111119
public static function export( array $errors, array $warnings, $format, array $args = array() ) {
112120
$format = strtolower( (string) $format );
113121

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

@@ -132,6 +140,10 @@ public static function export( array $errors, array $warnings, $format, array $a
132140
$content = self::to_markdown( $grouped, $args );
133141
$mime_type = 'text/markdown';
134142
break;
143+
case self::FORMAT_CTRF:
144+
$content = self::to_ctrf_json( $flat, $args );
145+
$mime_type = 'application/json';
146+
break;
135147
case self::FORMAT_CSV:
136148
default:
137149
$content = self::to_csv( $flat );
@@ -165,6 +177,8 @@ private static function build_filename( $slug, $timestamp, $format ) {
165177
$extension = $format;
166178
if ( self::FORMAT_MARKDOWN === $format ) {
167179
$extension = 'md';
180+
} elseif ( self::FORMAT_CTRF === $format ) {
181+
$extension = 'ctrf.json';
168182
}
169183

170184
return sprintf( '%1$s-%2$s.%3$s', $normalized_slug, $timestamp, $extension );
@@ -189,6 +203,133 @@ private static function to_json( array $grouped_results, array $args ) {
189203
return wp_json_encode( $payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES );
190204
}
191205

206+
/**
207+
* Generates CTRF JSON content for the results.
208+
*
209+
* @since 2.0.0
210+
*
211+
* @param array $flat_results Flat list of results including the file name.
212+
* @param array $args Additional arguments.
213+
* @return string CTRF JSON export content.
214+
*/
215+
public static function to_ctrf_json( array $flat_results, array $args = array() ) {
216+
$timestamp_iso = isset( $args['timestamp_iso'] ) ? (string) $args['timestamp_iso'] : gmdate( 'c' );
217+
$start_time = isset( $args['start_timestamp_ms'] ) ? (int) $args['start_timestamp_ms'] : (int) round( microtime( true ) * 1000 );
218+
$stop_time = isset( $args['stop_timestamp_ms'] ) ? (int) $args['stop_timestamp_ms'] : $start_time;
219+
$stop_time = max( $start_time, $stop_time );
220+
$tests = array();
221+
foreach ( $flat_results as $item ) {
222+
$tests[] = self::build_ctrf_test( $item );
223+
}
224+
225+
$payload = array(
226+
'reportFormat' => 'CTRF',
227+
'specVersion' => '1.0.0',
228+
'timestamp' => $timestamp_iso,
229+
'generatedBy' => 'plugin-check',
230+
'results' => array(
231+
'tool' => array(
232+
'name' => 'plugin-check',
233+
),
234+
'summary' => self::build_ctrf_summary( count( $tests ), $start_time, $stop_time ),
235+
'tests' => $tests,
236+
),
237+
);
238+
239+
return wp_json_encode( $payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES );
240+
}
241+
242+
/**
243+
* Builds CTRF summary payload.
244+
*
245+
* @since 2.0.0
246+
*
247+
* @param int $test_count Total number of findings.
248+
* @param int $start_time Start timestamp in milliseconds.
249+
* @param int $stop_time Stop timestamp in milliseconds.
250+
* @return array CTRF summary payload.
251+
*/
252+
private static function build_ctrf_summary( $test_count, $start_time, $stop_time ) {
253+
return array(
254+
'tests' => $test_count,
255+
'passed' => 0,
256+
'failed' => $test_count,
257+
'skipped' => 0,
258+
'pending' => 0,
259+
'other' => 0,
260+
'start' => $start_time,
261+
'stop' => $stop_time,
262+
);
263+
}
264+
265+
/**
266+
* Builds CTRF labels for a single finding.
267+
*
268+
* @since 2.0.0
269+
*
270+
* @param array $item Finding data.
271+
* @param string $code Normalized finding code.
272+
* @param string $type Normalized finding type.
273+
* @return array<string, bool|int|float|string> CTRF labels.
274+
*/
275+
private static function build_ctrf_labels( array $item, $code, $type ) {
276+
$labels = array(
277+
'code' => $code,
278+
);
279+
280+
$optional_labels = array(
281+
'findingType' => '' !== $type ? $type : null,
282+
'severity' => isset( $item['severity'] ) && '' !== $item['severity'] ? (int) $item['severity'] : null,
283+
'docs' => ! empty( $item['docs'] ) ? (string) $item['docs'] : null,
284+
'link' => ! empty( $item['link'] ) ? (string) $item['link'] : null,
285+
);
286+
287+
return array_merge(
288+
$labels,
289+
array_filter(
290+
$optional_labels,
291+
static function ( $value ) {
292+
return null !== $value;
293+
}
294+
)
295+
);
296+
}
297+
298+
/**
299+
* Builds a single CTRF test entry.
300+
*
301+
* @since 2.0.0
302+
*
303+
* @param array $item Finding data.
304+
* @return array CTRF test payload.
305+
*/
306+
private static function build_ctrf_test( array $item ) {
307+
$line = isset( $item['line'] ) ? (int) $item['line'] : 0;
308+
$column = isset( $item['column'] ) ? (int) $item['column'] : 0;
309+
$file = isset( $item['file'] ) ? (string) $item['file'] : '';
310+
$code = isset( $item['code'] ) ? (string) $item['code'] : 'unknown_code';
311+
$type = isset( $item['type'] ) ? (string) $item['type'] : '';
312+
$message = isset( $item['message'] ) ? wp_strip_all_tags( (string) $item['message'] ) : '';
313+
314+
return array(
315+
'name' => sprintf( '%1$s (%2$s:%3$d:%4$d)', $code, $file, $line, $column ),
316+
'status' => 'failed',
317+
'duration' => 0,
318+
'message' => $message,
319+
'line' => $line,
320+
'rawStatus' => $type,
321+
'filePath' => $file,
322+
'type' => 'static-analysis',
323+
'extra' => self::build_ctrf_labels( $item, $code, $type ),
324+
'suite' => array_filter(
325+
array(
326+
'plugin-check',
327+
$file,
328+
)
329+
),
330+
);
331+
}
332+
192333
/**
193334
* Generates CSV content for the results.
194335
*

tests/behat/features/plugin-check.feature

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,28 @@ Feature: Test that the WP-CLI command works.
166166
FILE:
167167
"""
168168

169+
When I run the WP-CLI command `plugin check foo-single.php --format=ctrf`
170+
Then STDOUT should be valid JSON
171+
And STDOUT should contain:
172+
"""
173+
"reportFormat": "CTRF"
174+
"""
175+
And STDOUT should not contain:
176+
"""
177+
FILE:
178+
"""
179+
180+
When I run the WP-CLI command `plugin check foo-single.php --format=strict-ctrf`
181+
Then STDOUT should be valid JSON
182+
And STDOUT should contain:
183+
"""
184+
"reportFormat": "CTRF"
185+
"""
186+
And STDOUT should not contain:
187+
"""
188+
FILE:
189+
"""
190+
169191
Scenario: Check plugin with special chars in plugin name
170192
Given a WP install with the Plugin Check plugin
171193
And a wp-content/plugins/johns-post-counter/johns-post-counter.php file:

0 commit comments

Comments
 (0)