Skip to content
Open
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
87 changes: 87 additions & 0 deletions ProcessMaker/Helpers/ScreenTemplateHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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;
}
}
12 changes: 9 additions & 3 deletions ProcessMaker/Templates/ScreenTemplate.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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)
Expand Down
120 changes: 113 additions & 7 deletions tests/Feature/Templates/Api/ScreenTemplateTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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);
}
}
}
Loading