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
95 changes: 88 additions & 7 deletions source/compose.manager/include/ComposeManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -2279,6 +2279,76 @@ function cancelDesc(myID) {
$("#" + myID).tooltipster("close");
}

var composeYamlSchemaCache = null;
var composeYamlCustomTagPattern = /(^|[\s:[{,\-])!(override|reset|merge)\b/m;

function getComposeYamlLibrary() {
if (typeof jsyaml !== 'undefined') {
return jsyaml;
}

if (typeof window !== 'undefined' && window.jsyaml) {
return window.jsyaml;
}

return null;
}

function composeYamlContainsCustomTags(content) {
return composeYamlCustomTagPattern.test(content || '');
}

function buildComposeYamlSchema() {
var yamlLib = getComposeYamlLibrary();
if (!yamlLib || typeof yamlLib.Type !== 'function' || !yamlLib.DEFAULT_SCHEMA || typeof yamlLib.DEFAULT_SCHEMA.extend !== 'function') {
return null;
}

var customTags = ['!override', '!reset', '!merge'];
var kinds = ['scalar', 'sequence', 'mapping'];
var types = [];

customTags.forEach(function(tag) {
kinds.forEach(function(kind) {
types.push(new yamlLib.Type(tag, {
kind: kind,
resolve: function() {
return true;
},
construct: function(data) {
if (data === null || data === undefined) {
if (kind === 'sequence') return [];
if (kind === 'mapping') return {};
return '';
}
return data;
}
}));
});
});

return yamlLib.DEFAULT_SCHEMA.extend(types);
}

function loadComposeYaml(content) {
var input = content || '';
var yamlLib = getComposeYamlLibrary();

if (!yamlLib || typeof yamlLib.load !== 'function') {
throw new Error('YAML parser is unavailable. Please reload the page and try again.');
}

if (!composeYamlSchemaCache) {
composeYamlSchemaCache = buildComposeYamlSchema();
}

if (composeYamlSchemaCache) {
return yamlLib.load(input, {
schema: composeYamlSchemaCache
});
}
return yamlLib.load(input);
}
function applyDesc(myID) {
var newDesc = $("#newDesc" + myID).val();
var project = $("#" + myID).attr("data-scriptname");
Expand Down Expand Up @@ -2315,7 +2385,7 @@ function generateProfiles(myID, myProject = null) {
var rawComposefile = JSON.parse(rawComposefile);

if ((rawComposefile.result == 'success')) {
var main_doc = jsyaml.load(rawComposefile.content);
var main_doc = loadComposeYaml(rawComposefile.content);

for (var service_key in main_doc.services) {
var service = main_doc.services[service_key];
Expand Down Expand Up @@ -4085,10 +4155,10 @@ function loadLabelsData() {
throw new Error('Failed to load compose file');
}

var mainDoc = jsyaml.load(composeData.content) || {
var mainDoc = loadComposeYaml(composeData.content) || {
services: {}
};
var overrideDoc = jsyaml.load(overrideData.content || '') || {
var overrideDoc = loadComposeYaml(overrideData.content || '') || {
services: {}
};

Expand All @@ -4099,7 +4169,9 @@ function loadLabelsData() {

editorModal.labelsData = {
mainDoc: mainDoc,
overrideDoc: overrideDoc
overrideDoc: overrideDoc,
overrideContent: overrideData.content || '',
overrideHasCustomTags: composeYamlContainsCustomTags(overrideData.content || '')
};

renderLabelsUI(mainDoc, overrideDoc);
Expand Down Expand Up @@ -4279,7 +4351,7 @@ function validateYaml(type, content) {

try {
if (content.trim()) {
jsyaml.load(content);
loadComposeYaml(content);
}
updateValidation(type, content, true);
} catch (e) {
Expand Down Expand Up @@ -4422,7 +4494,7 @@ function saveAllChanges() {

// Save labels if modified
if (editorModal.modifiedLabels.size > 0) {
savePromises.push(saveLabels());
savePromises.push(saveLabels(saveErrors));
}

$.when.apply($, savePromises).then(function() {
Expand Down Expand Up @@ -4576,13 +4648,20 @@ function saveSettings(saveErrors) {
}

// Save labels to override file
function saveLabels() {
function saveLabels(saveErrors) {
var project = editorModal.currentProject;

if (!editorModal.labelsData) {
return $.Deferred().reject().promise();
}

if (editorModal.labelsData.overrideHasCustomTags) {
if (saveErrors) {
saveErrors.push('WebUI labels cannot be saved because compose.override.yaml uses !override, !reset, or !merge tags. Edit the override file directly to preserve those tags.');
}
return $.Deferred().resolve(false).promise();
}

var mainDoc = editorModal.labelsData.mainDoc;
var overrideDoc = editorModal.labelsData.overrideDoc;

Expand Down Expand Up @@ -4634,6 +4713,8 @@ function saveLabels() {
editorModal.originalLabels[serviceKey + '_webui'] = $('#label-' + serviceKey + '-webui').val() || '';
editorModal.originalLabels[serviceKey + '_shell'] = $('#label-' + serviceKey + '-shell').val() || '';
}
editorModal.labelsData.overrideContent = rawOverride;
editorModal.labelsData.overrideHasCustomTags = false;
editorModal.modifiedLabels.clear();
updateTabModifiedState();
updateSaveButtonState();
Expand Down
17 changes: 17 additions & 0 deletions tests/unit/ComposeManagerMainSourceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,21 @@ public function testDockerLoadMapStoresParsedLimitBytes(): void
$this->assertStringContainsString('var memPair = parseMemUsagePair(parts[2]);', $source);
$this->assertStringContainsString('memLimitBytes: memPair.limit,', $source);
}

public function testComposeCustomTagSchemaSupportIsDeclared(): void
{
$source = $this->getPageSource();
$this->assertStringContainsString("var customTags = ['!override', '!reset', '!merge'];", $source);
$this->assertStringContainsString('function buildComposeYamlSchema()', $source);
$this->assertStringContainsString("if (typeof jsyaml !== 'undefined') {", $source);
$this->assertStringContainsString("throw new Error('YAML parser is unavailable. Please reload the page and try again.');", $source);
}

public function testLabelSaveBlocksTaggedOverrideRewrite(): void
{
$source = $this->getPageSource();
$this->assertStringContainsString('overrideHasCustomTags: composeYamlContainsCustomTags(overrideData.content || \'\')', $source);
$this->assertStringContainsString('WebUI labels cannot be saved because compose.override.yaml uses !override, !reset, or !merge tags.', $source);
}

}
Loading