diff --git a/php-transformer/src/HtmlToBlocks/Style/FormLayoutGraphBuilder.php b/php-transformer/src/HtmlToBlocks/Style/FormLayoutGraphBuilder.php index 7e781f7ea..970fc8b24 100644 --- a/php-transformer/src/HtmlToBlocks/Style/FormLayoutGraphBuilder.php +++ b/php-transformer/src/HtmlToBlocks/Style/FormLayoutGraphBuilder.php @@ -6,6 +6,7 @@ use Automattic\BlocksEngine\PhpTransformer\Css\CssRuleAnalyzer; use Automattic\BlocksEngine\PhpTransformer\Css\CssSelectorMatcher; use Automattic\BlocksEngine\PhpTransformer\Css\CssValueSplitter; +use Automattic\BlocksEngine\PhpTransformer\Path\ArtifactPath; use DOMElement; use InvalidArgumentException; @@ -207,7 +208,7 @@ private static function assertFacts(array $layout, array $provenance, ?array $co throw new InvalidArgumentException('Form layout graph provenance exceeds its limit.'); } foreach ( $provenance as $fact ) { - if ( ! is_array($fact) || ! is_string($fact['source_path'] ?? null) || ! preg_match('~^(?!.*(?:^|/)\.\.(?:/|$))[A-Za-z0-9._/-]+$~', $fact['source_path']) || ! preg_match('/^[a-f0-9]{64}$/', $fact['source_sha256'] ?? '') || ! is_string($fact['selector'] ?? null) || '' === trim($fact['selector']) || strlen($fact['selector']) > 1024 || ! is_array($fact['properties'] ?? null) || array() === $fact['properties'] || count($fact['properties']) > count($properties) || array_filter($fact['properties'], static fn (mixed $property): bool => ! is_string($property) || ! in_array($property, $properties, true) || ! isset($layout[self::layoutKey($property)])) || ($condition !== null && $fact['condition'] !== $condition) || ($condition === null && ($fact['condition'] ?? null) !== null) ) { + if ( ! is_array($fact) || ! is_string($fact['source_path'] ?? null) || '' === ArtifactPath::safeRelativePath($fact['source_path']) || ArtifactPath::safeRelativePath($fact['source_path']) !== $fact['source_path'] || ! preg_match('/^[a-f0-9]{64}$/', $fact['source_sha256'] ?? '') || ! is_string($fact['selector'] ?? null) || '' === trim($fact['selector']) || strlen($fact['selector']) > 1024 || ! is_array($fact['properties'] ?? null) || array() === $fact['properties'] || count($fact['properties']) > count($properties) || array_filter($fact['properties'], static fn (mixed $property): bool => ! is_string($property) || ! in_array($property, $properties, true) || ! isset($layout[self::layoutKey($property)])) || ($condition !== null && $fact['condition'] !== $condition) || ($condition === null && ($fact['condition'] ?? null) !== null) ) { throw new InvalidArgumentException('Form layout graph provenance is invalid.'); } } diff --git a/php-transformer/src/HtmlToBlocks/Style/FormPresentationGraphBuilder.php b/php-transformer/src/HtmlToBlocks/Style/FormPresentationGraphBuilder.php index ef7c66531..a402a6bfd 100644 --- a/php-transformer/src/HtmlToBlocks/Style/FormPresentationGraphBuilder.php +++ b/php-transformer/src/HtmlToBlocks/Style/FormPresentationGraphBuilder.php @@ -5,6 +5,7 @@ use Automattic\BlocksEngine\PhpTransformer\Css\CssRuleAnalyzer; use Automattic\BlocksEngine\PhpTransformer\Css\CssSelectorMatcher; +use Automattic\BlocksEngine\PhpTransformer\Path\ArtifactPath; use Closure; use DOMDocument; use DOMElement; @@ -170,7 +171,7 @@ private static function assertProvenance(array $provenance, array $styles, ?arra { if ( count($provenance) > self::MAX_PROVENANCE ) throw new InvalidArgumentException('Form presentation provenance exceeds its limit.'); foreach ( $provenance as $fact ) { - if ( ! is_array($fact) || ! is_string($fact['source_path'] ?? null) || ! preg_match('~^(?!.*(?:^|/)\.\.(?:/|$))[A-Za-z0-9._/-]+$~', $fact['source_path']) || ! preg_match('/^[a-f0-9]{64}$/', $fact['source_sha256'] ?? '') || ! is_string($fact['selector'] ?? null) || '' === trim($fact['selector']) || strlen($fact['selector']) > 1024 || ! is_array($fact['properties'] ?? null) || array() === $fact['properties'] || array_filter($fact['properties'], static fn (mixed $property): bool => ! is_string($property) || ! in_array($property, self::PROPERTIES, true) || ! isset($styles[self::key($property)])) || ($condition !== null && ($fact['condition'] ?? null) !== $condition) || ($condition === null && ($fact['condition'] ?? null) !== null) ) throw new InvalidArgumentException('Form presentation provenance is invalid.'); + if ( ! is_array($fact) || ! is_string($fact['source_path'] ?? null) || '' === ArtifactPath::safeRelativePath($fact['source_path']) || ArtifactPath::safeRelativePath($fact['source_path']) !== $fact['source_path'] || ! preg_match('/^[a-f0-9]{64}$/', $fact['source_sha256'] ?? '') || ! is_string($fact['selector'] ?? null) || '' === trim($fact['selector']) || strlen($fact['selector']) > 1024 || ! is_array($fact['properties'] ?? null) || array() === $fact['properties'] || array_filter($fact['properties'], static fn (mixed $property): bool => ! is_string($property) || ! in_array($property, self::PROPERTIES, true) || ! isset($styles[self::key($property)])) || ($condition !== null && ($fact['condition'] ?? null) !== $condition) || ($condition === null && ($fact['condition'] ?? null) !== null) ) throw new InvalidArgumentException('Form presentation provenance is invalid.'); } } diff --git a/php-transformer/tests/contract/run.php b/php-transformer/tests/contract/run.php index 24f2708d8..dd7fc8203 100644 --- a/php-transformer/tests/contract/run.php +++ b/php-transformer/tests/contract/run.php @@ -1293,6 +1293,11 @@ public function recognize(DOMElement $element, PatternContext $context): ?Patter $projectedDeepGraph = $deepDeclaration['payload']['entities'][0]['layout_graph'] ?? array(); $projectedWidthNodes = array_values(array_filter($projectedDeepGraph['nodes'] ?? array(), static fn(array $node): bool => 'td' === ($node['source']['tag'] ?? null) && '33.333333333333%' === ($node['layout']['width'] ?? null))); $assert(3 === count($projectedWidthNodes) && false === ($projectedDeepGraph['truncated'] ?? null), 'artifact compilation projects the complete deep percentage-width graph into generic/forms/v1.'); +$safeProvenanceArtifact = (new ArtifactCompiler())->compile(array('entrypoint' => 'index.html', 'files' => array('index.html' => '' . $layoutGraphHtml, 'assets/form style.css' => '.form{display:grid}')))->toArray(); +$safeProvenanceDeclaration = current(array_filter($safeProvenanceArtifact['source_reports']['wordpress_site_plan']['runtime_declarations'] ?? array(), static fn(array $declaration): bool => 'forms' === ($declaration['type'] ?? null))); +$safeProvenanceGraph = $safeProvenanceDeclaration['payload']['entities'][0]['layout_graph'] ?? array(); +$safeProvenanceNode = array_column($safeProvenanceGraph['nodes'] ?? array(), null, 'id')['form'] ?? array(); +$assert('assets/form style.css' === ($safeProvenanceNode['provenance'][0]['source_path'] ?? null), 'artifact compilation accepts canonical stylesheet paths in emitted form layout provenance.'); $depthBoundaryHtml = '
' . str_repeat('
', 16) . '' . str_repeat('
', 16) . '
'; $depthOverflowHtml = '
' . str_repeat('
', 17) . '' . str_repeat('
', 17) . '
'; $depthBoundaryGraph = (new HtmlTransformer())->transform($depthBoundaryHtml, array('static_css' => 'input{width:100%}'))->toArray()['fallbacks'][0]['layout_graph'] ?? array();