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
15 changes: 15 additions & 0 deletions front/assets/js/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,19 @@ export class Utils {
static endOfWeek(date) {
return moment(date).endOf('isoWeek');
}

// Escapes CSS attribute values
// Uses CSS.escape() when available, with fallback for older browsers
// Reference: https://developer.mozilla.org/en-US/docs/Web/API/CSS/escape
static escapeCSSAttributeValue(value) {
if (!value) return value;

// Use native CSS.escape() if available
if (typeof CSS !== 'undefined' && CSS.escape) {
return CSS.escape(value);
}

// Fallback for older browsers: escape quotes and backslashes
return value.replace(/(["'\\])/g, '\\$1');
}
}
93 changes: 93 additions & 0 deletions front/assets/js/utils.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,97 @@ describe("Utils", () => {
expect(Utils.toSeconds(36000000000)).to.equal(36)
});
});
describe("escapeCSSAttributeValue", () => {
it("escapes single quotes", () => {
const input = "Publish 'my-package' to Production";
const escaped = Utils.escapeCSSAttributeValue(input);
const expected = "Publish \\'my-package\\' to Production";
expect(escaped).to.equal(expected);
});

it("escapes double quotes", () => {
const input = 'Deploy "production" build';
const escaped = Utils.escapeCSSAttributeValue(input);
const expected = 'Deploy \\"production\\" build';
expect(escaped).to.equal(expected);
});

it("escapes backslashes", () => {
const input = "Path\\to\\file";
const escaped = Utils.escapeCSSAttributeValue(input);
const expected = "Path\\\\to\\\\file";
expect(escaped).to.equal(expected);
});

it("escapes special characters", () => {
const input = "test[data]:value.class#id";
const escaped = Utils.escapeCSSAttributeValue(input);
// When CSS.escape is available, it escapes brackets, colons, etc.
// Fallback only escapes quotes and backslashes
if (typeof CSS !== 'undefined' && CSS.escape) {
expect(escaped).to.not.equal(input);
} else {
expect(escaped).to.equal(input);
}
});

it("handles simple strings", () => {
const input = "Simple-promotion_name123";
const escaped = Utils.escapeCSSAttributeValue(input);
// Simple alphanumeric strings with hyphens/underscores shouldn't need escaping
expect(escaped).to.equal(input);
});

it("handles empty string", () => {
expect(Utils.escapeCSSAttributeValue("")).to.equal("");
});

it("handles null/undefined", () => {
expect(Utils.escapeCSSAttributeValue(null)).to.equal(null);
expect(Utils.escapeCSSAttributeValue(undefined)).to.equal(undefined);
});

it("escapes complex strings with brackets and quotes", () => {
const input = "test'][arbitrary-selector][data-x='some-value";
const escaped = Utils.escapeCSSAttributeValue(input);
const expected = "test\\'][arbitrary-selector][data-x=\\'some-value";
expect(escaped).to.equal(expected);
});

it("can be used in DOM selectors", () => {
const input = "Publish 'my-package' to Production";
const escaped = Utils.escapeCSSAttributeValue(input);
const expected = "Publish \\'my-package\\' to Production";
expect(escaped).to.equal(expected);

expect(() => {
document.querySelector(`[data-promotion-target="${escaped}"]`);
}).to.not.throw();
});

it("handles unicode characters (emoji)", () => {
const input = "Deploy πŸš€ to production";
const escaped = Utils.escapeCSSAttributeValue(input);
expect(escaped).to.equal(input);
});

it("handles unicode characters (accented letters)", () => {
const input = "DΓ©ploiement en franΓ§ais";
const escaped = Utils.escapeCSSAttributeValue(input);
expect(escaped).to.equal(input);
});

it("handles unicode characters (Chinese)", () => {
const input = "ιƒ¨η½²εˆ°η”ŸδΊ§ηŽ―ε’ƒ";
const escaped = Utils.escapeCSSAttributeValue(input);
expect(escaped).to.equal(input);
});

it("handles mixed unicode and special characters", () => {
const input = "Deploy 'app' πŸš€ to Prod";
const escaped = Utils.escapeCSSAttributeValue(input);
const expected = "Deploy \\'app\\' πŸš€ to Prod";
expect(escaped).to.equal(expected);
});
});
})
32 changes: 20 additions & 12 deletions front/assets/js/workflow_view/switch.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import $ from "jquery"
import { TriggerEvent } from "./trigger_event"
import { Pollman } from "../pollman"
import { TargetParams } from "./target_params"
import { Utils } from "../utils"

export var Switch = {
init: function() {
Expand Down Expand Up @@ -40,7 +41,8 @@ export var Switch = {
Switch.askToConfirmPromotion(switchId, promotionTarget);

// Focus on first input or select in the promotion box
const promotionForm = $(`[data-promotion-target="${promotionTarget}"]`);
const escapedPromotionTarget = Utils.escapeCSSAttributeValue(promotionTarget);
const promotionForm = $(`[data-promotion-target="${escapedPromotionTarget}"]`);
const firstInput = promotionForm.find('input, select').first();
if (firstInput.length) {
if (firstInput[0].tomselect) {
Expand Down Expand Up @@ -82,8 +84,9 @@ export var Switch = {
Pollman.start();
alert("Something went wrong. Please try again.");

$(`[switch='${parentPromotionSwitch}'] [data-promotion-target='${parentPromotionTarget}'][promote-button]`).removeAttr("disabled");
$(`[switch='${parentPromotionSwitch}'] [data-promotion-target='${parentPromotionTarget}'][promote-button]`).removeClass("btn-working");
let escapedTarget = Utils.escapeCSSAttributeValue(parentPromotionTarget);
$(`[switch='${parentPromotionSwitch}'] [data-promotion-target='${escapedTarget}'][promote-button]`).removeAttr("disabled");
$(`[switch='${parentPromotionSwitch}'] [data-promotion-target='${escapedTarget}'][promote-button]`).removeClass("btn-working");
});
})
},
Expand All @@ -96,9 +99,10 @@ export var Switch = {

let promotionTarget = Switch.parentPromotionTarget(target)
let promotionSwitch = Switch.parentSwitch(target)
let escapedTarget = Utils.escapeCSSAttributeValue(promotionTarget);

$(`[switch='${promotionSwitch}'] [data-promotion-target='${promotionTarget}'][promote-confirmation]`).hide();
$(`[switch='${promotionSwitch}'] [data-promotion-target='${promotionTarget}'][promote-button]`).show();
$(`[switch='${promotionSwitch}'] [data-promotion-target='${escapedTarget}'][promote-confirmation]`).hide();
$(`[switch='${promotionSwitch}'] [data-promotion-target='${escapedTarget}'][promote-button]`).show();

Pollman.pollNow();
Pollman.start();
Expand Down Expand Up @@ -138,18 +142,21 @@ export var Switch = {

askToConfirmPromotion: function(promotionSwitch, promotionTarget) {
Switch.hidePromotionBoxElements(promotionSwitch, promotionTarget);
$(`[switch='${promotionSwitch}'] [data-promotion-target='${promotionTarget}'][promote-confirmation]`).show();
let escapedTarget = Utils.escapeCSSAttributeValue(promotionTarget);
$(`[switch='${promotionSwitch}'] [data-promotion-target='${escapedTarget}'][promote-confirmation]`).show();
},

hidePromotionBoxElements: function(promotionSwitch, promotionTarget) {
$(`[switch='${promotionSwitch}'] [promotion-box][data-promotion-target='${promotionTarget}']`).children().hide();
let escapedTarget = Utils.escapeCSSAttributeValue(promotionTarget);
$(`[switch='${promotionSwitch}'] [promotion-box][data-promotion-target='${escapedTarget}']`).children().hide();
},

showPromotingInProgress(promotionSwitch, promotionTarget) {
$(`[switch='${promotionSwitch}'] [data-promotion-target='${promotionTarget}'][promote-confirmation]`).hide();
$(`[switch='${promotionSwitch}'] [data-promotion-target='${promotionTarget}'][promote-button]`).show();
$(`[switch='${promotionSwitch}'] [data-promotion-target='${promotionTarget}'][promote-button]`).attr("disabled", "");
$(`[switch='${promotionSwitch}'] [data-promotion-target='${promotionTarget}'][promote-button]`).addClass("btn-working");
let escapedTarget = Utils.escapeCSSAttributeValue(promotionTarget);
$(`[switch='${promotionSwitch}'] [data-promotion-target='${escapedTarget}'][promote-confirmation]`).hide();
$(`[switch='${promotionSwitch}'] [data-promotion-target='${escapedTarget}'][promote-button]`).show();
$(`[switch='${promotionSwitch}'] [data-promotion-target='${escapedTarget}'][promote-button]`).attr("disabled", "");
$(`[switch='${promotionSwitch}'] [data-promotion-target='${escapedTarget}'][promote-button]`).addClass("btn-working");
Switch.afterResize(promotionSwitch);
},

Expand All @@ -167,7 +174,8 @@ export var Switch = {
},

latestTriggerEvent: function(promotionSwitch, promotionTarget) {
return $(`[switch='${promotionSwitch}'] [trigger-event][data-promotion-target='${promotionTarget}']`).first();
let escapedTarget = Utils.escapeCSSAttributeValue(promotionTarget);
return $(`[switch='${promotionSwitch}'] [trigger-event][data-promotion-target='${escapedTarget}']`).first();
},

isProcessed: function(triggerEvent) {
Expand Down
Loading