diff --git a/ProcessMaker/Helpers/ScreenTemplateHelper.php b/ProcessMaker/Helpers/ScreenTemplateHelper.php index 67a3684c3f..b6af576270 100644 --- a/ProcessMaker/Helpers/ScreenTemplateHelper.php +++ b/ProcessMaker/Helpers/ScreenTemplateHelper.php @@ -4,6 +4,32 @@ class ScreenTemplateHelper { + private const RENDERABLE_STRING_FIELDS = [ + 'ariaLabel', + 'content', + 'fieldValue', + 'helper', + 'label', + 'loadingLabel', + 'placeholder', + ]; + + /** + * Remove serialized Vue component definitions from screen config. + * + * Screen templates can contain old inspector metadata where inspector.type + * is a serialized Vue component object. That data is not needed at runtime + * and can reach renderer paths that expect Mustache templates to be strings. + */ + public static function sanitizeScreenConfig($config): array + { + if (!is_array($config)) { + return []; + } + + return self::sanitizeConfigValue($config); + } + /** * Remove screen components from the configuration based on the provided components. * @@ -402,4 +428,65 @@ public static function generateCss($cssArray) return $cssString; } + + private static function sanitizeConfigValue($value, ?string $key = null) + { + if ($key === 'validation' && is_array($value) && $value === []) { + return null; + } + + if (in_array($key, self::RENDERABLE_STRING_FIELDS, true)) { + return self::sanitizeRenderableString($value); + } + + if (!is_array($value)) { + return $value; + } + + if (array_is_list($value)) { + return array_map(fn ($item) => self::sanitizeConfigValue($item), $value); + } + + $sanitized = []; + foreach ($value as $childKey => $childValue) { + if ($childKey === 'inspector' && is_array($childValue)) { + $sanitized[$childKey] = array_map( + fn ($item) => self::sanitizeInspectorItem($item), + $childValue + ); + continue; + } + + $sanitized[$childKey] = self::sanitizeConfigValue($childValue, (string) $childKey); + } + + return $sanitized; + } + + private static function sanitizeInspectorItem($item) + { + if (!is_array($item)) { + return $item; + } + + $sanitized = []; + foreach ($item as $key => $value) { + if ($key === 'type' && is_array($value)) { + continue; + } + + $sanitized[$key] = self::sanitizeConfigValue($value, (string) $key); + } + + return $sanitized; + } + + private static function sanitizeRenderableString($value): ?string + { + if ($value === null || is_array($value)) { + return null; + } + + return is_string($value) ? $value : (string) $value; + } } diff --git a/ProcessMaker/Templates/ScreenTemplate.php b/ProcessMaker/Templates/ScreenTemplate.php index 742cd5048d..964eb303eb 100644 --- a/ProcessMaker/Templates/ScreenTemplate.php +++ b/ProcessMaker/Templates/ScreenTemplate.php @@ -854,8 +854,11 @@ private function mergeFields($screen, $currentScreenPage, $newTemplateScreen, $t throw new MissingScreenPageException(); } - $templateComponents = ScreenTemplateHelper::getScreenComponents($newTemplateScreen->config, - $supportedComponents, false)[0]['items']; + // Sanitize only imported template components so existing screen config is not altered. + $templateComponents = ScreenTemplateHelper::sanitizeScreenConfig( + ScreenTemplateHelper::getScreenComponents($newTemplateScreen->config, + $supportedComponents, false)[0]['items'] ?? [] + ); $screenConfig[$currentScreenPage]['items'] = array_merge($screenConfig[$currentScreenPage]['items'], $templateComponents); @@ -866,11 +869,14 @@ private function mergeFields($screen, $currentScreenPage, $newTemplateScreen, $t private function getTemplateComponents($newTemplateScreen, $templateOptions, $supportedComponents) { - return !in_array('Fields', $templateOptions) + $templateComponents = !in_array('Fields', $templateOptions) ? ScreenTemplateHelper::getScreenComponents($newTemplateScreen->config, $supportedComponents, false)[0]['items'] ?? [] : $newTemplateScreen->config[0]['items'] ?? []; + + // Sanitize only imported template components so existing screen config is not altered. + return ScreenTemplateHelper::sanitizeScreenConfig($templateComponents); } private function setScreenConfig($screen) diff --git a/tests/Feature/Templates/Api/ScreenTemplateTest.php b/tests/Feature/Templates/Api/ScreenTemplateTest.php index 36e684888d..08cc543e05 100644 --- a/tests/Feature/Templates/Api/ScreenTemplateTest.php +++ b/tests/Feature/Templates/Api/ScreenTemplateTest.php @@ -392,13 +392,11 @@ public function testSharedTemplateAuthorization() public function testApplyCssToExistingScreen() { - // Create a new screen with two pages and no custom_css - $screenPath = base_path(self::SCREEN_PATH); - $screenData = json_decode(File::get($screenPath), true); - + // Create a new screen with no config and no custom_css $screen = Screen::factory()->create([ - 'config' => $screenData, + 'config' => null, ]); + $this->assertNull($screen->config); $this->assertNull($screen->custom_css); // Create a screen template with custom_css @@ -420,13 +418,14 @@ public function testApplyCssToExistingScreen() $response->assertStatus(200); if (class_exists(VersionHistory::class)) { - $updatedScreen = \ProcessMaker\Models\ScreenVersion::select('custom_css')->where('screen_id', $screen->id)->latest()->firstOrFail(); + $updatedScreen = \ProcessMaker\Models\ScreenVersion::select('config', 'custom_css')->where('screen_id', $screen->id)->latest()->firstOrFail(); } else { - $updatedScreen = Screen::select('custom_css')->where('id', $screen->id)->firstOrFail(); + $updatedScreen = Screen::select('config', 'custom_css')->where('id', $screen->id)->firstOrFail(); } // Check that the screen has the custom_css $this->assertNotNull($updatedScreen->custom_css); + $this->assertNull($updatedScreen->config); } public function testApplyFieldsToExistingScreen() @@ -475,6 +474,43 @@ public function testApplyFieldsToExistingScreen() // Check that the screen config is not empty $this->assertNotEmpty($updatedScreen->config[1]['items']); + $this->assertScreenConfigSanitized($updatedScreen->config); + } + + public function testApplyTemplateSanitizesSerializedInspectorComponents() + { + $templatePath = base_path(self::SCREEN_TEMPLATE_PATH); + $screenTemplateData = File::get($templatePath); + + $screenTemplate = $this->createScreenTemplateFromManifest($screenTemplateData); + + $screenPath = base_path(self::SCREEN_PATH); + $screenData = json_decode(File::get($screenPath), true); + + $newScreen = Screen::factory()->create([ + 'config' => $screenData, + ]); + + $route = route('api.template.applyTemplate', [ + 'type' => 'screen', + 'id' => $screenTemplate->id, + ]); + + $response = $this->apiCall('POST', $route, [ + 'screenId' => $newScreen->id, + 'templateOptions' => ['Fields', 'Layout'], + 'currentScreenPage' => 1, + ]); + + $response->assertStatus(200); + + if (class_exists(VersionHistory::class)) { + $updatedScreen = \ProcessMaker\Models\ScreenVersion::select('config')->where('screen_id', $newScreen->id)->latest()->firstOrFail(); + } else { + $updatedScreen = Screen::select('config')->where('id', $newScreen->id)->firstOrFail(); + } + + $this->assertScreenConfigSanitized($updatedScreen->config); } public function testApplyLayoutToExistingScreen() @@ -574,4 +610,74 @@ public function testApplyFullTemplateToExistingScreen() // Check that the screen has the custom_css $this->assertNotNull($updatedScreen->custom_css); } + + private function assertScreenConfigSanitized(array $config): void + { + foreach ($config as $page) { + $this->assertScreenConfigValueSanitized($page); + } + } + + private function createScreenTemplateFromManifest(string $manifest): ScreenTemplates + { + $screenTemplate = ScreenTemplates::make([ + 'unique_template_id' => 'serialized-inspector-regression', + 'name' => 'Serialized Inspector Regression', + 'description' => 'Template with serialized Vue component inspector metadata.', + 'version' => '1.0.0', + 'user_id' => null, + 'editing_screen_uuid' => null, + 'screen_category_id' => ScreenCategory::factory()->create()->getKey(), + 'screen_type' => 'FORM', + 'media_collection' => 'serialized-inspector-regression-media', + 'manifest' => $manifest, + 'screen_custom_css' => null, + 'is_public' => true, + 'is_default_template' => false, + 'is_system' => false, + 'asset_type' => null, + ]); + $screenTemplate->saveOrFail(); + + return $screenTemplate; + } + + private function assertScreenConfigValueSanitized($value): void + { + if (!is_array($value)) { + return; + } + + if (array_key_exists('inspector', $value) && is_array($value['inspector'])) { + foreach ($value['inspector'] as $inspector) { + if (is_array($inspector) && array_key_exists('type', $inspector)) { + $this->assertFalse(is_array($inspector['type'])); + } + } + } + + foreach (['content', 'label'] as $field) { + if (!array_key_exists($field, $value)) { + continue; + } + + $this->assertFalse(is_array($value[$field])); + } + + if ( + array_key_exists('tooltip', $value) + && is_array($value['tooltip']) + && array_key_exists('content', $value['tooltip']) + ) { + $this->assertFalse(is_array($value['tooltip']['content'])); + } + + if (array_key_exists('validation', $value) && is_array($value['validation'])) { + $this->assertNotEmpty($value['validation']); + } + + foreach ($value as $childValue) { + $this->assertScreenConfigValueSanitized($childValue); + } + } }