Skip to content
Closed
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
26 changes: 21 additions & 5 deletions source/compose.manager/event/started
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,33 @@ fi

for dir in $COMPOSE_ROOT/*; do
if [ -d "$dir" ]; then
if [ -f "$dir/docker-compose.yml" ] || [ -f "$dir/indirect" ]; then
# Check if any valid compose file exists
if [ -f "$dir/compose.yaml" ] || [ -f "$dir/compose.yml" ] || [ -f "$dir/docker-compose.yaml" ] || [ -f "$dir/docker-compose.yml" ] || [ -f "$dir/indirect" ]; then
if [ -f "$dir/autostart" ]; then
autostart=${dir}/autostart
if [ 'true' == "$(< "${autostart}" )" ]; then
name=${dir}/name
name=$(< "${name}")
name=$(sanitize ${name})
# Find the compose file
compose_file=""
if [ -f "$dir/compose.yaml" ]; then
compose_file="$dir/compose.yaml"
override_file="$dir/compose.override.yml"
elif [ -f "$dir/compose.yml" ]; then
compose_file="$dir/compose.yml"
override_file="$dir/compose.override.yml"
elif [ -f "$dir/docker-compose.yaml" ]; then
compose_file="$dir/docker-compose.yaml"
override_file="$dir/docker-compose.override.yml"
elif [ -f "$dir/docker-compose.yml" ]; then
compose_file="$dir/docker-compose.yml"
override_file="$dir/docker-compose.override.yml"
fi

override=""
if [ -f "$dir/docker-compose.override.yml" ]; then
override="$dir/docker-compose.override.yml"
if [ -n "$compose_file" ] && [ -f "$override_file" ]; then
override="$override_file"
override="-f ${override@Q}"
fi
Comment on lines +37 to 57
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Override selection is incorrect for .yaml compose files and indirect stacks. For compose.yaml / docker-compose.yaml this sets override_file to a .yml name, so .override.yaml files won’t be picked up. Also, for indirect stacks (only $dir/indirect present), compose_file stays empty and the override is skipped entirely due to -n "$compose_file", which is a regression from the previous docker-compose.override.yml check. Consider: (1) deriving the override filename from the chosen compose file’s extension/base name (or checking both .yml and .yaml), and (2) allowing the plugin override in the project dir even when the stack is indirect.

Copilot uses AI. Check for mistakes.
envpath=""
Expand All @@ -50,8 +67,7 @@ for dir in $COMPOSE_ROOT/*; do
indirect=$(< "${indirect}")
eval $COMPOSE_WRAPPER -c up -d ${indirect@Q} -p ${name} $recreate $debug $override $envpath > /dev/null &
else
dir="$dir/docker-compose.yml"
eval $COMPOSE_WRAPPER -c up -f ${dir@Q} -p ${name} $recreate $debug $override $envpath > /dev/null &
eval $COMPOSE_WRAPPER -c up -f ${compose_file@Q} -p ${name} $recreate $debug $override $envpath > /dev/null &
fi
fi
fi
Expand Down
26 changes: 21 additions & 5 deletions source/compose.manager/event/stopping_docker
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,29 @@ COMPOSE_WRAPPER=/usr/local/emhttp/plugins/compose.manager/scripts/compose.sh

for dir in $COMPOSE_ROOT/*; do
if [ -d "$dir" ]; then
if [ -f "$dir/docker-compose.yml" ] || [ -f "$dir/indirect" ]; then
# Check if any valid compose file exists
if [ -f "$dir/compose.yaml" ] || [ -f "$dir/compose.yml" ] || [ -f "$dir/docker-compose.yaml" ] || [ -f "$dir/docker-compose.yml" ] || [ -f "$dir/indirect" ]; then
name=${dir}/name
name=$(< "${name}")
# Find the compose file
compose_file=""
if [ -f "$dir/compose.yaml" ]; then
compose_file="$dir/compose.yaml"
override_file="$dir/compose.override.yml"
elif [ -f "$dir/compose.yml" ]; then
compose_file="$dir/compose.yml"
override_file="$dir/compose.override.yml"
elif [ -f "$dir/docker-compose.yaml" ]; then
compose_file="$dir/docker-compose.yaml"
override_file="$dir/docker-compose.override.yml"
elif [ -f "$dir/docker-compose.yml" ]; then
compose_file="$dir/docker-compose.yml"
override_file="$dir/docker-compose.override.yml"
fi

override=""
if [ -f "$dir/docker-compose.override.yml" ]; then
override="$dir/docker-compose.override.yml"
if [ -n "$compose_file" ] && [ -f "$override_file" ]; then
override="$override_file"
override="-f ${override@Q}"
fi
Comment on lines +15 to 35
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue as event/started: for .yaml compose files this hard-codes a .yml override name, and for indirect stacks compose_file is empty so overrides in the project directory are never applied. This can prevent label overrides from being used and change stopping behavior compared to previous versions. Update override detection to support .override.yaml and to include the project’s override even when indirect is used.

Copilot uses AI. Check for mistakes.
envpath=""
Expand All @@ -28,8 +45,7 @@ for dir in $COMPOSE_ROOT/*; do
indirect=$(< "${indirect}")
eval $COMPOSE_WRAPPER -c stop -d ${indirect@Q} -p "${name// /_}" $override $envpath > /dev/null &
else
dir="$dir/docker-compose.yml"
eval $COMPOSE_WRAPPER -c stop -f ${dir@Q} -p "${name// /_}" $override $envpath > /dev/null &
eval $COMPOSE_WRAPPER -c stop -f ${compose_file@Q} -p "${name// /_}" $override $envpath > /dev/null &
fi
fi
fi
Expand Down
26 changes: 12 additions & 14 deletions source/compose.manager/php/compose_manager_main.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ function createComboButton($text, $id, $onClick, $onClickParams, $items) {
}
$o = "";
foreach ($composeProjects as $project) {
if ( ( ! is_file("$compose_root/$project/docker-compose.yml") ) &&
if ( ( findComposeFile("$compose_root/$project") === null ) &&
( ! is_file("$compose_root/$project/indirect") ) ) {
continue;
}
Expand Down Expand Up @@ -596,7 +596,9 @@ function editComposeFile(myID) {
editor.getSession().setOptions({ tabSize: 2, useSoftTabs: true });

$('#editorFileName').data("stackname", project);
$('#editorFileName').data("stackfilename", "docker-compose.yml")
// Extract the actual filename from the response
var filename = response.fileName.split('/').pop();
$('#editorFileName').data("stackfilename", filename)
$('#editorFileName').html(response.fileName)
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

response.fileName is inserted into the DOM via $('#editorFileName').html(response.fileName) without any HTML-encoding, but its value ultimately depends on user-controlled paths (e.g., an indirect stack path set via stackPath). An attacker can supply a path containing HTML/JavaScript (such as <script> tags) so that when the compose editor is opened, arbitrary script executes in the admin’s browser. To mitigate this, treat response.fileName as plain text (e.g., use a text-only setter or explicit HTML-escaping) rather than inserting it as raw HTML.

Copilot uses AI. Check for mistakes.
$(".editing").show();
window.scrollTo(0, 0);
Expand Down Expand Up @@ -635,18 +637,14 @@ function saveEdit() {
var scriptContents = editor.getValue();
var actionStr = null

switch(fileName) {
case 'docker-compose.yml':
actionStr = 'saveYml'
break;

case '.env':
actionStr = 'saveEnv'
break;

default:
$(".editing").hide();
return;
// Check if this is a compose file (any valid extension)
if (fileName.match(/^(docker-)?compose\.(yml|yaml)$/)) {
actionStr = 'saveYml'
} else if (fileName === '.env') {
actionStr = 'saveEnv'
} else {
$(".editing").hide();
return;
}

$.post(caURL,{action:actionStr,script:project,scriptContents:scriptContents},function(data) {
Expand Down
65 changes: 61 additions & 4 deletions source/compose.manager/php/compose_util.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ function logger($string) {

function execComposeCommandInTTY($cmd, $debug)
{
global $socket_name;;
global $socket_name;
$pid = exec("pgrep -a ttyd|awk '/\\/$socket_name\\.sock/{print \$1}'");
if ( $debug ) {
logger($pid);
Expand Down Expand Up @@ -61,14 +61,71 @@ function echoComposeCommand($action)
$composeFile = "-d$composeFile";
}
else {
$composeFile .= "$path/docker-compose.yml";
$foundComposeFile = findComposeFile($path);
if ($foundComposeFile === null) {
$composeFile .= "$path/docker-compose.yml";
} else {
$composeFile .= $foundComposeFile;
}
$composeFile = "-f$composeFile";
}
$composeCommand[] = $composeFile;

if ( is_file("$path/docker-compose.override.yml") ) {
$composeOverride = "-f$path/docker-compose.override.yml";
// First, always include the plugin's override file if it exists
global $compose_root;
$projectName = basename($path);
$pluginOverrideYml = "$compose_root/$projectName/docker-compose.override.yml";
$pluginOverrideYaml = "$compose_root/$projectName/docker-compose.override.yaml";

if (is_file($pluginOverrideYml)) {
$composeOverride = "-f$pluginOverrideYml";
$composeCommand[] = $composeOverride;
if ( $debug ) {
logger("Using plugin override file: $pluginOverrideYml");
}
} else if (is_file($pluginOverrideYaml)) {
$composeOverride = "-f$pluginOverrideYaml";
$composeCommand[] = $composeOverride;
if ( $debug ) {
logger("Using plugin override file: $pluginOverrideYaml");
}
}

// Then, also include any project-specific override files
if (isIndirect($path)) {
$basePath = getPath($path);
} else {
$basePath = $path;
}

$foundComposeFile = findComposeFile($basePath);
if ($foundComposeFile !== null) {
$baseFileName = getComposeFileBaseName($foundComposeFile);
// Get the extension of the original compose file
$extension = pathinfo($foundComposeFile, PATHINFO_EXTENSION);
$overrideFile = "$basePath/$baseFileName.override.$extension";
if (is_file($overrideFile)) {
$composeOverride = "-f$overrideFile";
$composeCommand[] = $composeOverride;
if ( $debug ) {
logger("Using project override file: $overrideFile");
}
}
} else {
// Check for both yml and yaml override files
if (is_file("$basePath/docker-compose.override.yml")) {
$composeOverride = "-f$basePath/docker-compose.override.yml";
$composeCommand[] = $composeOverride;
if ( $debug ) {
logger("Using project override file: $basePath/docker-compose.override.yml");
}
} else if (is_file("$basePath/docker-compose.override.yaml")) {
$composeOverride = "-f$basePath/docker-compose.override.yaml";
$composeCommand[] = $composeOverride;
if ( $debug ) {
logger("Using project override file: $basePath/docker-compose.override.yaml");
Comment on lines +109 to +126
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The override resolution can add the same override file twice. For example, if the base compose file is docker-compose.yml and $compose_root/<project>/docker-compose.override.yml exists, it’s added once in the “plugin override” block and again in the “project override” block ($overrideFile). Consider de-duping -f entries and/or clearly separating “plugin labels override” from “user override” to avoid redundant arguments and ambiguity about precedence.

Suggested change
$composeCommand[] = $composeOverride;
if ( $debug ) {
logger("Using project override file: $overrideFile");
}
}
} else {
// Check for both yml and yaml override files
if (is_file("$basePath/docker-compose.override.yml")) {
$composeOverride = "-f$basePath/docker-compose.override.yml";
$composeCommand[] = $composeOverride;
if ( $debug ) {
logger("Using project override file: $basePath/docker-compose.override.yml");
}
} else if (is_file("$basePath/docker-compose.override.yaml")) {
$composeOverride = "-f$basePath/docker-compose.override.yaml";
$composeCommand[] = $composeOverride;
if ( $debug ) {
logger("Using project override file: $basePath/docker-compose.override.yaml");
if (!in_array($composeOverride, $composeCommand, true)) {
$composeCommand[] = $composeOverride;
if ( $debug ) {
logger("Using project override file: $overrideFile");
}
} else if ( $debug ) {
logger("Skipping duplicate project override file: $overrideFile");
}
}
} else {
// Check for both yml and yaml override files
if (is_file("$basePath/docker-compose.override.yml")) {
$composeOverride = "-f$basePath/docker-compose.override.yml";
if (!in_array($composeOverride, $composeCommand, true)) {
$composeCommand[] = $composeOverride;
if ( $debug ) {
logger("Using project override file: $basePath/docker-compose.override.yml");
}
} else if ( $debug ) {
logger("Skipping duplicate project override file: $basePath/docker-compose.override.yml");
}
} else if (is_file("$basePath/docker-compose.override.yaml")) {
$composeOverride = "-f$basePath/docker-compose.override.yaml";
if (!in_array($composeOverride, $composeCommand, true)) {
$composeCommand[] = $composeOverride;
if ( $debug ) {
logger("Using project override file: $basePath/docker-compose.override.yaml");
}
} else if ( $debug ) {
logger("Skipping duplicate project override file: $basePath/docker-compose.override.yaml");

Copilot uses AI. Check for mistakes.
}
}
}

if ( is_file("$path/envpath") ) {
Expand Down
75 changes: 67 additions & 8 deletions source/compose.manager/php/exec.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,13 @@ function getElement($element) {
#Create stack files
if ( !empty($indirect) ) {
file_put_contents("$folder/indirect",$indirect);
if ( !is_file("$indirect/docker-compose.yml") ) {
$composeFile = findComposeFile($indirect);
if ($composeFile === null) {
// Create default docker-compose.yml if no compose file exists
file_put_contents("$indirect/docker-compose.yml","services:\n");
}
} else {
// Create default docker-compose.yml for new projects
file_put_contents("$folder/docker-compose.yml","services:\n");
}

Expand Down Expand Up @@ -92,9 +95,22 @@ function getElement($element) {
case 'getYml':
$script = isset($_POST['script']) ? urldecode(($_POST['script'])) : "";
$basePath = getPath("$compose_root/$script");
$fileName = "docker-compose.yml";
$composeFile = findComposeFile($basePath);

// If no compose file exists, use the default path
if ($composeFile === null) {
// Try both yaml and yml extensions
if (is_file("$basePath/docker-compose.yaml")) {
$fileName = "docker-compose.yaml";
} else {
$fileName = "docker-compose.yml";
}
Comment on lines +100 to +107
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In getYml, the branch that checks is_file("$basePath/docker-compose.yaml") is unreachable: if that file existed, findComposeFile($basePath) would have returned it. This extra logic is confusing and can be removed/simplified (e.g., pick a single default when no compose file exists).

Suggested change
// If no compose file exists, use the default path
if ($composeFile === null) {
// Try both yaml and yml extensions
if (is_file("$basePath/docker-compose.yaml")) {
$fileName = "docker-compose.yaml";
} else {
$fileName = "docker-compose.yml";
}
// If no compose file exists, use a single default path
if ($composeFile === null) {
$fileName = "docker-compose.yml";

Copilot uses AI. Check for mistakes.
$composeFile = "$basePath/$fileName";
} else {
$fileName = basename($composeFile);
}

$scriptContents = file_get_contents("$basePath/$fileName");
$scriptContents = file_get_contents($composeFile);
$scriptContents = str_replace("\r","",$scriptContents);
if ( ! $scriptContents ) {
$scriptContents = "services:\n";
Expand All @@ -120,7 +136,22 @@ function getElement($element) {
case 'getOverride':
$script = isset($_POST['script']) ? urldecode(($_POST['script'])) : "";
$basePath = "$compose_root/$script";
$fileName = "docker-compose.override.yml";
$composeFile = findComposeFile($basePath);

// Determine the override file name based on the base compose file
if ($composeFile !== null) {
$baseFileName = getComposeFileBaseName($composeFile);
// Get the extension of the original compose file
$extension = pathinfo($composeFile, PATHINFO_EXTENSION);
$fileName = "$baseFileName.override.$extension";
} else {
// Check for both yml and yaml override files
if (is_file("$basePath/docker-compose.override.yaml")) {
$fileName = "docker-compose.override.yaml";
} else {
$fileName = "docker-compose.override.yml";
}
}

$scriptContents = is_file("$basePath/$fileName") ? file_get_contents("$basePath/$fileName") : "";
$scriptContents = str_replace("\r","",$scriptContents);
Expand All @@ -133,9 +164,22 @@ function getElement($element) {
$script = isset($_POST['script']) ? urldecode(($_POST['script'])) : "";
$scriptContents = isset($_POST['scriptContents']) ? $_POST['scriptContents'] : "";
$basePath = getPath("$compose_root/$script");
$fileName = "docker-compose.yml";
$composeFile = findComposeFile($basePath);

// If no compose file exists, use the default path
if ($composeFile === null) {
// Try both yaml and yml extensions
if (is_file("$basePath/docker-compose.yaml")) {
$fileName = "docker-compose.yaml";
} else {
$fileName = "docker-compose.yml";
}
Comment on lines +169 to +176
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as getYml: in saveYml, checking for docker-compose.yaml inside the ($composeFile === null) branch is dead code because findComposeFile($basePath) would have found it. Simplifying this will make the save path behavior clearer.

Suggested change
// If no compose file exists, use the default path
if ($composeFile === null) {
// Try both yaml and yml extensions
if (is_file("$basePath/docker-compose.yaml")) {
$fileName = "docker-compose.yaml";
} else {
$fileName = "docker-compose.yml";
}
// If no compose file exists, use a default path
if ($composeFile === null) {
$fileName = "docker-compose.yml";

Copilot uses AI. Check for mistakes.
$composeFile = "$basePath/$fileName";
} else {
$fileName = basename($composeFile);
}

file_put_contents("$basePath/$fileName",$scriptContents);
file_put_contents($composeFile, $scriptContents);
echo "$basePath/$fileName saved";
break;
case 'saveEnv':
Expand All @@ -155,9 +199,24 @@ function getElement($element) {
$script = isset($_POST['script']) ? urldecode(($_POST['script'])) : "";
$scriptContents = isset($_POST['scriptContents']) ? $_POST['scriptContents'] : "";
$basePath = "$compose_root/$script";
$fileName = "docker-compose.override.yml";
$composeFile = findComposeFile($basePath);

// Determine the override file name based on the base compose file
if ($composeFile !== null) {
$baseFileName = getComposeFileBaseName($composeFile);
// Get the extension of the original compose file
$extension = pathinfo($composeFile, PATHINFO_EXTENSION);
$fileName = "$baseFileName.override.$extension";
} else {
// Check for both yml and yaml override files
if (is_file("$basePath/docker-compose.override.yaml")) {
$fileName = "docker-compose.override.yaml";
} else {
$fileName = "docker-compose.override.yml";
}
}

file_put_contents("$basePath/$fileName",$scriptContents);
file_put_contents("$basePath/$fileName", $scriptContents);
echo "$basePath/$fileName saved";
break;
case 'updateAutostart':
Expand Down
35 changes: 35 additions & 0 deletions source/compose.manager/php/util.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,40 @@
<?php

/**
* Find the first valid compose file in a directory following Docker Compose priority order
*
* @param string $dir Directory to search in
* @return string|null Path to the first valid compose file found, or null if none found
*/
function findComposeFile($dir) {
// Docker Compose priority order: compose.yaml, compose.yml, docker-compose.yaml, docker-compose.yml
$possibleFiles = [
"$dir/compose.yaml",
"$dir/compose.yml",
"$dir/docker-compose.yaml",
"$dir/docker-compose.yml"
];

foreach ($possibleFiles as $file) {
if (file_exists($file)) {
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

findComposeFile() uses file_exists(), which will also return true for directories. Since callers assume the result is readable YAML (e.g., pass it to file_get_contents() / pathinfo()), this can lead to warnings or incorrect behavior if a directory happens to match one of these names. Prefer is_file() (and optionally is_link() if symlinks are expected).

Suggested change
if (file_exists($file)) {
if (is_file($file)) {

Copilot uses AI. Check for mistakes.
return $file;
}
}

return null;
}

/**
* Get the base name of a compose file without extension
*
* @param string $composeFilePath Full path to compose file
* @return string Base name (e.g., "compose" or "docker-compose")
*/
function getComposeFileBaseName($composeFilePath) {
$filename = basename($composeFilePath);
return pathinfo($filename, PATHINFO_FILENAME);
}

function sanitizeStr($a) {
$a = str_replace(".","_",$a);
$a = str_replace(" ","_",$a);
Expand Down
Loading