diff --git a/.github/workflows/secrets-scan.yml b/.github/workflows/secrets-scan.yml index 049c02f..f9db69f 100644 --- a/.github/workflows/secrets-scan.yml +++ b/.github/workflows/secrets-scan.yml @@ -1,29 +1,29 @@ -name: Secrets Scan -on: - pull_request: - types: [opened, synchronize, reopened] -jobs: - security-secrets: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: '2' - ref: '${{ github.event.pull_request.head.ref }}' - - run: | - git reset --soft HEAD~1 - - name: Install Talisman - run: | - # Download Talisman - wget https://github.com/thoughtworks/talisman/releases/download/v1.37.0/talisman_linux_amd64 -O talisman - - # Checksum verification - checksum=$(sha256sum ./talisman | awk '{print $1}') - if [ "$checksum" != "8e0ae8bb7b160bf10c4fa1448beb04a32a35e63505b3dddff74a092bccaaa7e4" ]; then exit 1; fi - - # Make it executable - chmod +x talisman - - name: Run talisman - run: | - # Run Talisman with the pre-commit hook +name: Secrets Scan +on: + pull_request: + types: [opened, synchronize, reopened] +jobs: + security-secrets: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: '2' + ref: '${{ github.event.pull_request.head.ref }}' + - run: | + git reset --soft HEAD~1 + - name: Install Talisman + run: | + # Download Talisman + wget https://github.com/thoughtworks/talisman/releases/download/v1.37.0/talisman_linux_amd64 -O talisman + + # Checksum verification + checksum=$(sha256sum ./talisman | awk '{print $1}') + if [ "$checksum" != "8e0ae8bb7b160bf10c4fa1448beb04a32a35e63505b3dddff74a092bccaaa7e4" ]; then exit 1; fi + + # Make it executable + chmod +x talisman + - name: Run talisman + run: | + # Run Talisman with the pre-commit hook ./talisman --githook pre-commit \ No newline at end of file diff --git a/CODEOWNERS b/CODEOWNERS index 1be7e0d..0773923 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1 +1 @@ -* @contentstack/security-admin +* @contentstack/security-admin \ No newline at end of file diff --git a/__test__/entry-editable.test.ts b/__test__/entry-editable.test.ts index 53a4882..39d72e4 100644 --- a/__test__/entry-editable.test.ts +++ b/__test__/entry-editable.test.ts @@ -1,7 +1,5 @@ -import { EntryModel } from '../src' import { addTags } from '../src/entry-editable' -import { entry_global_field, entry_global_field_multiple, entry_modular_block, entry_reference, entry_with_text } from './mock/entry-editable-mock' -import { entryMultipleContent } from './mock/entry-multiple-rich-text-content' +import { entry_global_field, entry_global_field_multiple, entry_modular_block, entry_reference, entry_with_text, entry_with_applied_variants, entry_with_parent_path_variants } from './mock/entry-editable-mock' describe('Entry editable test', () => { it('Entry with text test', done => { @@ -129,4 +127,147 @@ describe('Entry editable test', () => { done() }) + // Tests for applied variants functionality + describe('Applied Variants Tests', () => { + it('Entry with applied variants should generate v2 tags with variant suffix', done => { + addTags(entry_with_applied_variants, 'entry_asset', false) + + // Field with direct variant match should get v2 prefix and variant suffix + expect((entry_with_applied_variants as any)['$']['rich_text_editor']).toEqual('data-cslp=v2:entry_asset.entry_uid_1_variant_1.en-us.rich_text_editor') + + // Nested field with direct variant match + expect((entry_with_applied_variants as any)['nested']['$']['field']).toEqual('data-cslp=v2:entry_asset.entry_uid_1_variant_2.en-us.nested.field') + + // Field without variant should not have v2 prefix + expect((entry_with_applied_variants as any)['nested']['$']['other_field']).toEqual('data-cslp=entry_asset.entry_uid_1.en-us.nested.other_field') + expect((entry_with_applied_variants as any)['$']['rich_text_editor_multiple']).toEqual('data-cslp=entry_asset.entry_uid_1.en-us.rich_text_editor_multiple') + + done() + }) + + it('Entry with applied variants should return v2 objects when tagsAsObject is true', done => { + addTags(entry_with_applied_variants, 'entry_asset', true) + + // Field with direct variant match should get v2 prefix and variant suffix as object + expect((entry_with_applied_variants as any)['$']['rich_text_editor']).toEqual({'data-cslp': 'v2:entry_asset.entry_uid_1_variant_1.en-us.rich_text_editor'}) + + // Nested field with direct variant match + expect((entry_with_applied_variants as any)['nested']['$']['field']).toEqual({'data-cslp': 'v2:entry_asset.entry_uid_1_variant_2.en-us.nested.field'}) + + // Field without variant should not have v2 prefix + expect((entry_with_applied_variants as any)['nested']['$']['other_field']).toEqual({'data-cslp': 'entry_asset.entry_uid_1.en-us.nested.other_field'}) + expect((entry_with_applied_variants as any)['$']['rich_text_editor_multiple']).toEqual({'data-cslp': 'entry_asset.entry_uid_1.en-us.rich_text_editor_multiple'}) + + done() + }) + + it('Entry with parent path variants should find correct variant', done => { + addTags(entry_with_parent_path_variants, 'entry_asset', false) + + // Group field should get parent variant + expect((entry_with_parent_path_variants as any)['$']['group']).toEqual('data-cslp=v2:entry_asset.entry_uid_3_parent_variant.en-us.group') + // Field under 'group' parent should get parent variant + expect((entry_with_parent_path_variants as any)['group']['$']['other']).toEqual('data-cslp=v2:entry_asset.entry_uid_3_parent_variant.en-us.group.other') + // Field under 'group.nested' should get parent variant (group is longer match) + expect((entry_with_parent_path_variants as any)['group']['nested']['$']['field']).toEqual('data-cslp=v2:entry_asset.entry_uid_3_parent_variant.en-us.group.nested.field') + // Field with exact deep path match should get deep variant + expect((entry_with_parent_path_variants as any)['group']['nested']['deep']['$']['field']).toEqual('data-cslp=v2:entry_asset.entry_uid_3_deep_variant.en-us.group.nested.deep.field') + + // Field with the same starting path should not get parent variant + expect((entry_with_parent_path_variants as any)['$']['group_multiple']).toEqual('data-cslp=entry_asset.entry_uid_3.en-us.group_multiple') + + // Modular block content with variant should get v2 prefix and variant suffix + expect((entry_with_parent_path_variants as any)['modular_blocks'][0]['$']['content']).toEqual('data-cslp=v2:entry_asset.entry_uid_3_parent_variant.en-us.modular_blocks.0.content') + // Modular block field inside a variantised parent should get v2 prefix and variant suffix + expect((entry_with_parent_path_variants as any)['modular_blocks'][0]['content']['$']['title']).toEqual('data-cslp=v2:entry_asset.entry_uid_3_parent_variant.en-us.modular_blocks.0.content.title') + + // Modular block content without variant should not have v2 prefix and variant suffix + expect((entry_with_parent_path_variants as any)['modular_blocks'][1]['$']['content']).toEqual('data-cslp=entry_asset.entry_uid_3.en-us.modular_blocks.1.content') + // Modular block field inside a non variantised parent should not get v2 prefix and variant suffix + expect((entry_with_parent_path_variants as any)['modular_blocks'][1]['content']['$']['title']).toEqual('data-cslp=entry_asset.entry_uid_3.en-us.modular_blocks.1.content.title') + + done() + }) + + it('Entry with modular block variants should apply variants correctly', done => { + addTags(entry_with_applied_variants, 'entry_asset', false) + + // Modular block content with variant should get v2 prefix and variant suffix + expect((entry_with_applied_variants as any)['modular_blocks'][1]['$']['content_from_variant']).toEqual('data-cslp=v2:entry_asset.entry_uid_1_variant_3.en-us.modular_blocks.1.content_from_variant') + // Modular block field inside a variantised parent should get v2 prefix and variant suffix + expect((entry_with_applied_variants as any)['modular_blocks'][1]['content_from_variant']['$']['title']).toEqual('data-cslp=v2:entry_asset.entry_uid_1_variant_3.en-us.modular_blocks.1.content_from_variant.title') + // Field inside a variantised parent with a different variant should get v2 prefix and variant suffix of that variant + expect((entry_with_applied_variants as any)['modular_blocks'][1]['content_from_variant']['$']['different_from_parent_variant']).toEqual('data-cslp=v2:entry_asset.entry_uid_1_variant_4.en-us.modular_blocks.1.content_from_variant.different_from_parent_variant') + + // Modular block content without variant should get v2 prefix and variant suffix + expect((entry_with_applied_variants as any)['modular_blocks'][0]['$']['content']).toEqual('data-cslp=entry_asset.entry_uid_1.en-us.modular_blocks.0.content') + // Modular block field without variant should not have v2 prefix and variant suffix + expect((entry_with_applied_variants as any)['modular_blocks'][0]['content']['$']['title']).toEqual('data-cslp=entry_asset.entry_uid_1.en-us.modular_blocks.0.content.title') + + done() + }) + + it('Entry without applied variants should work normally', done => { + addTags(entry_with_text, 'entry_asset', false) + + // Should not have v2 prefix when no variants are applied + expect((entry_with_text as any)['$']['rich_text_editor']).toEqual('data-cslp=entry_asset.entry_uid_1.en-us.rich_text_editor') + expect((entry_with_text as any)['$']['rich_text_editor_multiple']).toEqual('data-cslp=entry_asset.entry_uid_1.en-us.rich_text_editor_multiple') + + done() + }) + + it('Entry with empty applied variants should work normally', done => { + const entryWithEmptyVariants = { + ...entry_with_text, + _applied_variants: {} + } + + addTags(entryWithEmptyVariants, 'entry_asset', false) + + // Should not have v2 prefix when variants object is empty + expect((entryWithEmptyVariants as any)['$']['rich_text_editor']).toEqual('data-cslp=entry_asset.entry_uid_1.en-us.rich_text_editor') + expect((entryWithEmptyVariants as any)['$']['rich_text_editor_multiple']).toEqual('data-cslp=entry_asset.entry_uid_1.en-us.rich_text_editor_multiple') + + done() + }) + + it('Variant path sorting should work correctly for nested paths', done => { + const entryWithComplexVariants = { + "_version": 10, + "locale": "en-us", + "uid": "entry_uid_test", + "ACL": {}, + "_applied_variants": { + "a": "variant_a", + "a.b": "variant_ab", + "a.b.c": "variant_abc", + "a.b.c.d": "variant_abcd" + }, + "a": { + "b": { + "c": { + "d": { + "field": "deep field" + }, + "field": "c field" + }, + "field": "b field" + }, + "field": "a field" + } + } + + addTags(entryWithComplexVariants, 'entry_asset', false) + + // Should use the longest matching path variant + expect((entryWithComplexVariants as any)['a']['b']['c']['d']['$']['field']).toEqual('data-cslp=v2:entry_asset.entry_uid_test_variant_abcd.en-us.a.b.c.d.field') + expect((entryWithComplexVariants as any)['a']['b']['c']['$']['field']).toEqual('data-cslp=v2:entry_asset.entry_uid_test_variant_abc.en-us.a.b.c.field') + expect((entryWithComplexVariants as any)['a']['b']['$']['field']).toEqual('data-cslp=v2:entry_asset.entry_uid_test_variant_ab.en-us.a.b.field') + expect((entryWithComplexVariants as any)['a']['$']['field']).toEqual('data-cslp=v2:entry_asset.entry_uid_test_variant_a.en-us.a.field') + + done() + }) + }) + }) \ No newline at end of file diff --git a/__test__/mock/entry-editable-mock.ts b/__test__/mock/entry-editable-mock.ts index 590c131..05b4659 100644 --- a/__test__/mock/entry-editable-mock.ts +++ b/__test__/mock/entry-editable-mock.ts @@ -22,9 +22,9 @@ const entry_modular_block = { "

module 2 

" ], "_metadata": { - "uid": "metadata_uid" + "uid": "metadata_uid_1" } - } + }, }, { "global_modular": { @@ -46,13 +46,13 @@ const entry_modular_block = { "

Module 2

" ], "_metadata": { - "uid": "metadata_uid" + "uid": "metadata_uid_1" } - } + }, } ], "_metadata": { - "uid": "metadata_uid" + "uid": "metadata_uid_2" } } } @@ -106,9 +106,9 @@ const entry_global_field = { "

global modular 2

" ], "_metadata": { - "uid": "metadata_uid" + "uid": "metadata_uid_1" } - } + }, } ] }, @@ -139,21 +139,113 @@ const entry_global_field_multiple = { "

Global multiple modular

\n

2

" ], "_metadata": { - "uid": "metadata_uid" + "uid": "metadata_uid_1" } - } + }, } ], "_metadata": { - "uid": "metadata_uid" + "uid": "metadata_uid_1" + } + } + ] +} +// Mock entry with applied variants for testing variant functionality +const entry_with_applied_variants = { + "_version": 10, + "locale": "en-us", + "uid": "entry_uid_1", + "ACL": {}, + "_applied_variants": { + "rich_text_editor": "variant_1", + "nested.field": "variant_2", + "modular_blocks.content_from_variant.metadata_uid_2": "variant_3", + "modular_blocks.content_from_variant.metadata_uid_2.different_from_parent_variant": "variant_4" + }, + "rich_text_editor": "

Content with variant

", + "rich_text_editor_multiple": [ + "

Multiple content with variant

" + ], + "nested": { + "field": "nested field content", + "other_field": "other nested content" + }, + "modular_blocks": [ + { + "content": { + "title": "modular title", + "_metadata": { + "uid": "metadata_uid_1" + } + } + }, + { + "content_from_variant": { + "title": "modular title from variant", + "different_from_parent_variant": "different from parent variant", + "_metadata": { + "uid": "metadata_uid_2" + } + } + } + ] +} + + +// Mock entry with nested parent path variants +const entry_with_parent_path_variants = { + "_version": 10, + "locale": "en-us", + "uid": "entry_uid_3", + "ACL": {}, + "_applied_variants": { + "group": "parent_variant", + "group.nested.deep": "deep_variant", + "modular_blocks.content.metadata_uid_1": "parent_variant" + }, + "group": { + "nested": { + "field": "nested field", + "deep": { + "field": "deep field" + } + }, + "other": "other field" + }, + "modular_blocks": [ + { + "content": { + "title": "modular title", + "_metadata": { + "uid": "metadata_uid_1" + } + }, + }, + { + "content": { + "title": "modular title 2", + "_metadata": { + "uid": "metadata_uid_2" + } + }, + } + ], + "group_multiple": [ + { + "other": "other field", + "_metadata": { + "uid": "metadata_uid_1" } } ] } + export { entry_with_text, entry_reference, entry_global_field, entry_modular_block, - entry_global_field_multiple + entry_global_field_multiple, + entry_with_applied_variants, + entry_with_parent_path_variants } \ No newline at end of file diff --git a/src/entry-editable.ts b/src/entry-editable.ts index 25d9b77..5cf9bd1 100644 --- a/src/entry-editable.ts +++ b/src/entry-editable.ts @@ -1,5 +1,11 @@ import { EntryModel } from "." +interface AppliedVariants { + _applied_variants: { [key: string]: any } + shouldApplyVariant: boolean + metaKey: string +} + export function addTags(entry: EntryModel, contentTypeUid: string, tagsAsObject: boolean, locale: string = 'en-us'): void { if (entry) { // handle case senstivity for contentTypeUid and locale @@ -11,7 +17,7 @@ export function addTags(entry: EntryModel, contentTypeUid: string, tagsAsObject: } } -function getTag(content: object, prefix: string, tagsAsObject: boolean, locale: string, appliedVariants: { _applied_variants: { [key: string]: any }, shouldApplyVariant: boolean, metaKey: string }): object { +function getTag(content: object, prefix: string, tagsAsObject: boolean, locale: string, appliedVariants: AppliedVariants): object { const tags: any = {} const { metaKey, shouldApplyVariant, _applied_variants } = appliedVariants Object.entries(content).forEach(([key, value]) => { @@ -115,12 +121,24 @@ function getTag(content: object, prefix: string, tagsAsObject: boolean, locale: } function getTagsValue(dataValue: string, tagsAsObject: boolean, appliedVariants: { _applied_variants: { [key: string]: any }, shouldApplyVariant: boolean, metaKey: string }): any { - if (appliedVariants.shouldApplyVariant && appliedVariants._applied_variants && appliedVariants._applied_variants[appliedVariants.metaKey]) { + if (appliedVariants.shouldApplyVariant && appliedVariants._applied_variants) { + const isFieldVariantised = appliedVariants._applied_variants[appliedVariants.metaKey]; + if(isFieldVariantised) { const variant = appliedVariants._applied_variants[appliedVariants.metaKey] // Adding v2 prefix to the cslp tag. New cslp tags are in v2 format. ex: v2:content_type_uid.entry_uid.locale.title const newDataValueArray = ('v2:' + dataValue).split('.'); newDataValueArray[1] = newDataValueArray[1] + '_' + variant; dataValue = newDataValueArray.join('.'); + } + else { + const parentVariantisedPath = getParentVariantisedPath(appliedVariants); + if(parentVariantisedPath) { + const variant = appliedVariants._applied_variants[parentVariantisedPath]; + const newDataValueArray = ('v2:' + dataValue).split('.'); + newDataValueArray[1] = newDataValueArray[1] + '_' + variant; + dataValue = newDataValueArray.join('.'); + } + } } if (tagsAsObject) { return { "data-cslp": dataValue }; @@ -135,4 +153,26 @@ function getParentTagsValue(dataValue: string, tagsAsObject: boolean): any { } else { return `data-cslp-parent-field=${dataValue}`; } +} + +function getParentVariantisedPath(appliedVariants: AppliedVariants) { + try { + // Safety fallback + if(!appliedVariants._applied_variants) return ''; + const variantisedFieldPaths = Object.keys(appliedVariants._applied_variants).sort((a, b) => { + return b.length - a.length; + }); + const childPathFragments = appliedVariants.metaKey.split('.'); + // Safety fallback + if(childPathFragments.length === 0 || variantisedFieldPaths.length === 0) return ''; + const parentVariantisedPath = variantisedFieldPaths.find(path => { + const parentFragments = path.split('.'); + if(parentFragments.length > childPathFragments.length) return false; + return parentFragments.every((fragment, index) => childPathFragments[index] === fragment); + }) ?? ''; + return parentVariantisedPath; + } + catch(e) { + return ''; + } } \ No newline at end of file