Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions php-transformer/src/Contract/ConversionReportProjection.php
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,12 @@ private static function fallbackDiagnostics(array $fallbacks): array
'type' => $fallback['type'] ?? '',
'reason' => $fallback['reason'] ?? '',
'diagnostic_code' => $fallback['diagnostic_code'] ?? '',
'severity' => $fallback['severity'] ?? '',
'runtime_requirement' => $fallback['runtime_requirement'] ?? '',
'recoverability' => $fallback['recoverability'] ?? '',
'actionability' => $fallback['actionability'] ?? '',
'suggested_primitive' => $fallback['suggested_primitive'] ?? '',
'materialization_hint' => $fallback['materialization_hint'] ?? '',
'source_format' => $fallback['source_format'] ?? '',
'source' => $fallback['source'] ?? '',
'scope' => $fallback['scope'] ?? '',
Expand Down
85 changes: 85 additions & 0 deletions php-transformer/src/HtmlToBlocks/FallbackDiagnostic.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<?php
declare(strict_types=1);

namespace Automattic\BlocksEngine\PhpTransformer\HtmlToBlocks;

final class FallbackDiagnostic
{
/**
* @param array<string, mixed> $fields
* @param array<string, mixed> $provenance
* @return array<string, mixed>
*/
public static function build(array $fields, array $provenance = array()): array
{
return array_merge(self::defaults($fields), $fields, $provenance);
}

/**
* @param array<string, mixed> $fields
* @return array<string, mixed>
*/
private static function defaults(array $fields): array
{
$code = (string) ($fields['diagnostic_code'] ?? '');

return match ( $code ) {
'html_form_fallback' => array(
'severity' => 'warning',
'runtime_requirement' => 'server_or_client_form_handler',
'recoverability' => 'recoverable_with_runtime_mapping',
'actionability' => 'map_form_action_controls_and_submission_handler',
'suggested_primitive' => 'form',
'materialization_hint' => 'preserve_form_markup_or_replace_with_form_block_integration',
),
'html_script_fallback' => array(
'severity' => 'warning',
'runtime_requirement' => 'client_script_execution',
'recoverability' => 'recoverable_with_script_enqueue_or_component_runtime',
'actionability' => 'review_script_source_and_enqueue_or_rebuild_behavior',
'suggested_primitive' => 'script_asset',
'materialization_hint' => 'enqueue_script_or_rebuild_as_interactive_block',
),
'html_inline_svg_fallback' => array(
'severity' => 'info',
'runtime_requirement' => 'none',
'recoverability' => 'recoverable_as_static_markup_or_image_asset',
'actionability' => 'review_sanitized_svg_and_materialize_as_image_or_html',
'suggested_primitive' => 'image_or_html',
'materialization_hint' => 'materialize_safe_svg_as_image_asset_or_core_html',
),
'html_unsafe_inline_svg' => array(
'severity' => 'warning',
'runtime_requirement' => 'sanitization_review',
'recoverability' => 'recoverable_after_security_review',
'actionability' => 'remove_scriptable_svg_content_or_replace_with_safe_asset',
'suggested_primitive' => 'image_asset',
'materialization_hint' => 'sanitize_svg_before_materializing_asset',
),
'html_iframe_embed_fallback' => array(
'severity' => 'warning',
'runtime_requirement' => 'third_party_embed_runtime',
'recoverability' => 'recoverable_with_embed_provider_or_html_preservation',
'actionability' => 'map_iframe_src_to_supported_embed_provider_or_preserve_html',
'suggested_primitive' => 'embed',
'materialization_hint' => 'convert_supported_src_to_core_embed_or_preserve_sanitized_iframe_html',
),
'html_unsupported_element' => array(
'severity' => 'info',
'runtime_requirement' => 'unknown',
'recoverability' => 'recoverable_with_manual_mapping',
'actionability' => 'map_element_to_supported_block_or_preserve_html',
'suggested_primitive' => 'core/html',
'materialization_hint' => 'preserve_sanitized_markup_until_a_specific_block_mapping_exists',
),
default => array(
'severity' => 'warning',
'runtime_requirement' => 'unknown',
'recoverability' => 'unknown',
'actionability' => 'review_fallback_metadata',
'suggested_primitive' => 'core/html',
'materialization_hint' => 'preserve_fallback_metadata_for_manual_review',
),
};
}
}
12 changes: 6 additions & 6 deletions php-transformer/src/HtmlToBlocks/HtmlTransformer.php
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ public function transform(string $html, array $options = array()): TransformerRe
),
);
$fallbacks = array(
array_merge(array(
FallbackDiagnostic::build(array(
'type' => 'html',
'reason' => 'parse_failed',
'diagnostic_code' => 'html_parse_failed',
Expand Down Expand Up @@ -440,7 +440,7 @@ private function convertElement(DOMElement $element, array &$fallbacks, bool $ca
if ( 'form' === $tagName ) {
$controls = $this->formControls($element);
$boundedHtml = $this->boundedFallbackHtml($this->safeFallbackHtml($element));
$fallbacks[] = array_merge(array(
$fallbacks[] = FallbackDiagnostic::build(array(
'type' => 'html',
'reason' => 'form_requires_runtime',
'diagnostic_code' => 'html_form_fallback',
Expand Down Expand Up @@ -533,7 +533,7 @@ private function convertElement(DOMElement $element, array &$fallbacks, bool $ca
$fallback['control'] = $control;
}

$fallbacks[] = array_merge($fallback, $this->fallbackProvenance);
$fallbacks[] = FallbackDiagnostic::build($fallback, $this->fallbackProvenance);
}

return null;
Expand Down Expand Up @@ -1194,7 +1194,7 @@ private function captureInlineSvgFallback(DOMElement $element, array &$fallbacks
$safe = $this->isSafeSvgContent($rawHtml);
$boundedHtml = $this->boundedFallbackHtml($this->safeFallbackHtml($element));

$fallbacks[] = array_merge(array(
$fallbacks[] = FallbackDiagnostic::build(array(
'type' => 'inline_svg',
'reason' => $safe ? 'inline_svg_fallback' : 'unsafe_inline_svg',
'diagnostic_code' => $safe ? 'html_inline_svg_fallback' : 'html_unsafe_inline_svg',
Expand All @@ -1220,7 +1220,7 @@ private function captureScriptFallback(DOMElement $element, array &$fallbacks):
{
$boundedHtml = $this->boundedFallbackHtml($this->safeFallbackHtml($element));
$boundedBody = $this->boundedFallbackText(trim($element->textContent ?? ''));
$fallbacks[] = array_merge(array(
$fallbacks[] = FallbackDiagnostic::build(array(
'type' => 'html',
'reason' => 'script_requires_runtime',
'diagnostic_code' => 'html_script_fallback',
Expand Down Expand Up @@ -1850,7 +1850,7 @@ private function convertIframeElement(DOMElement $iframe, array &$fallbacks): ?a
}

$boundedHtml = $this->boundedFallbackHtml($this->safeFallbackHtml($iframe));
$fallbacks[] = array_merge(array(
$fallbacks[] = FallbackDiagnostic::build(array(
'type' => 'html',
'reason' => 'iframe_embed_fallback',
'diagnostic_code' => 'html_iframe_embed_fallback',
Expand Down
26 changes: 25 additions & 1 deletion php-transformer/tests/contract/run.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,16 @@ function serialize_blocks(array $blocks): string
exit(1);
};

$assertNormalizedFallbackDiagnostic = static function (array $diagnostic, string $code, string $severity, string $runtimeRequirement, string $suggestedPrimitive) use ($assert): void {
$assert($code === ($diagnostic['diagnostic_code'] ?? ''), "conversion report exposes {$code} diagnostic code");
$assert($severity === ($diagnostic['severity'] ?? ''), "conversion report exposes {$code} severity");
$assert($runtimeRequirement === ($diagnostic['runtime_requirement'] ?? ''), "conversion report exposes {$code} runtime requirement");
$assert(isset($diagnostic['recoverability']) && '' !== $diagnostic['recoverability'], "conversion report exposes {$code} recoverability");
$assert(isset($diagnostic['actionability']) && '' !== $diagnostic['actionability'], "conversion report exposes {$code} actionability");
$assert($suggestedPrimitive === ($diagnostic['suggested_primitive'] ?? ''), "conversion report exposes {$code} suggested primitive");
$assert(isset($diagnostic['materialization_hint']) && '' !== $diagnostic['materialization_hint'], "conversion report exposes {$code} materialization hint");
};

$assertInvalidCanonicalEnvelope = static function (array $result, string $expectedMessage, string $message, bool $requireMaterializationPlan = false) use ($assert): void {
try {
TransformerResult::assertCanonicalEnvelope($result, $requireMaterializationPlan);
Expand Down Expand Up @@ -107,7 +117,7 @@ function serialize_blocks(array $blocks): string
)->toArray();
$formDiagnostic = $formFallback['source_reports']['conversion_report']['fallback_diagnostics'][0] ?? array();
$assert(array() === ($formFallback['blocks'] ?? array()), 'form fallback does not synthesize canonical blocks');
$assert('html_form_fallback' === ($formDiagnostic['diagnostic_code'] ?? ''), 'conversion report exposes form fallback diagnostic code');
$assertNormalizedFallbackDiagnostic($formDiagnostic, 'html_form_fallback', 'warning', 'server_or_client_form_handler', 'form');
$assert('/contact' === ($formDiagnostic['form']['action'] ?? ''), 'conversion report exposes form action metadata');
$assert('post' === ($formDiagnostic['form']['method'] ?? ''), 'conversion report exposes normalized form method metadata');
$assert(3 === ($formDiagnostic['control_count'] ?? null), 'conversion report exposes form control count');
Expand All @@ -117,6 +127,20 @@ function serialize_blocks(array $blocks): string
$assert('support' === ($formDiagnostic['controls'][1]['options'][0]['value'] ?? ''), 'conversion report exposes select option values');
$assert(is_int($formDiagnostic['html_bytes'] ?? null), 'conversion report exposes bounded fallback HTML byte size');

$normalizedFallbacks = ( new HtmlTransformer() )->transform(
'<main><svg><circle cx="5" cy="5" r="5"></circle></svg><svg><script>alert(1)</script></svg><script src="/app.js">init()</script><aside>Fallback</aside><iframe src="javascript:alert(1)"></iframe></main>'
)->toArray();
$normalizedDiagnostics = $normalizedFallbacks['source_reports']['conversion_report']['fallback_diagnostics'] ?? array();
$diagnosticsByCode = array();
foreach ( $normalizedDiagnostics as $diagnostic ) {
$diagnosticsByCode[$diagnostic['diagnostic_code'] ?? ''] = $diagnostic;
}
$assertNormalizedFallbackDiagnostic($diagnosticsByCode['html_inline_svg_fallback'] ?? array(), 'html_inline_svg_fallback', 'info', 'none', 'image_or_html');
$assertNormalizedFallbackDiagnostic($diagnosticsByCode['html_unsafe_inline_svg'] ?? array(), 'html_unsafe_inline_svg', 'warning', 'sanitization_review', 'image_asset');
$assertNormalizedFallbackDiagnostic($diagnosticsByCode['html_script_fallback'] ?? array(), 'html_script_fallback', 'warning', 'client_script_execution', 'script_asset');
$assertNormalizedFallbackDiagnostic($diagnosticsByCode['html_unsupported_element'] ?? array(), 'html_unsupported_element', 'info', 'unknown', 'core/html');
$assertNormalizedFallbackDiagnostic($diagnosticsByCode['html_iframe_embed_fallback'] ?? array(), 'html_iframe_embed_fallback', 'warning', 'third_party_embed_runtime', 'embed');

$assetMetadataOptions = array(
'context' => array(
'asset_metadata' => array(
Expand Down
Loading