Reviewing some common docker utilities

/usr/local/emhttp/plugins/dynamix.docker.manager/include/DockerClient.php
<?PHP
/* Copyright 2005-2025, Lime Technology
 * Copyright 2012-2025, Bergware International.
 * Copyright 2014-2021, Guilherme Jardim, Eric Schultz, Jon Panozzo.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License version 2,
 * as published by the Free Software Foundation.
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 */
?>
<?
$docroot ??= ($_SERVER['DOCUMENT_ROOT'] ?: '/usr/local/emhttp');
require_once "$docroot/webGui/include/Wrappers.php";
require_once "$docroot/plugins/dynamix.docker.manager/include/Helpers.php";

// add translations
if (_var($_SERVER,'REQUEST_URI')!='docker' && substr(_var($_SERVER,'REQUEST_URI'),0,7)!='/Docker') {
        $_SERVER['REQUEST_URI'] = 'docker';
        require_once "$docroot/webGui/include/Translations.php";
}

$dockerManPaths = [
        'autostart-file' => "/var/lib/docker/unraid-autostart",
        'update-status'  => "/var/lib/docker/unraid-update-status.json",
        'template-repos' => "/boot/config/plugins/dockerMan/template-repos",
        'templates-user' => "/boot/config/plugins/dockerMan/templates-user",
        'templates-usb'  => "/boot/config/plugins/dockerMan/templates",
        'images'         => "/var/lib/docker/unraid/images",
        'user-prefs'     => "/boot/config/plugins/dockerMan/userprefs.cfg",
        'plugin'         => "$docroot/plugins/dynamix.docker.manager",
        'images-ram'     => "$docroot/state/plugins/dynamix.docker.manager/images",
        'webui-info'     => "$docroot/state/plugins/dynamix.docker.manager/docker.json"
];

// get network drivers
$driver = DockerUtil::driver();

// Docker configuration file - guaranteed to exist
$docker_cfgfile = '/boot/config/docker.cfg';
$mgmt_port = ['br0','bond0','eth0'];

if (file_exists($docker_cfgfile)) {
        $port = DockerUtil::port();
        $port = strtoupper((in_array($port,$mgmt_port) && lan_port($port,true)==0) ? 'wlan0' : $port);
        exec("grep -Pom2 '_SUBNET_|_{$port}(_[0-9]+)?=' $docker_cfgfile",$cfg);
        if (isset($cfg[0]) && $cfg[0]=='_SUBNET_' && empty($cfg[1])) {
                # interface has changed, update configuration
                exec("sed -ri 's/_(BR0|BOND0|ETH0)(_[0-9]+)?=/_{$port}\\2=/' $docker_cfgfile");
        }
}

$defaults  = (array)@parse_ini_file("$docroot/plugins/dynamix.docker.manager/default.cfg");
$dockercfg = array_replace_recursive($defaults, (array)@parse_ini_file($docker_cfgfile));

function var_split($item, $i=0) {
        return array_pad(explode(' ',$item),$i+1,'')[$i];
}

#######################################
##       DOCKERTEMPLATES CLASS       ##
#######################################

class DockerTemplates {
        public $verbose = false;

        private function debug($m) {
                if ($this->verbose) echo $m."\n";
        }

        private function removeDir($path) {
                if (is_dir($path)) {
                        $files = array_diff(scandir($path), ['.', '..']);
                        foreach ($files as $file) {
                                $this->removeDir(realpath($path).'/'.$file);
                        }
                        return rmdir($path);
                } elseif (is_file($path)) return unlink($path);
                return false;
        }

        public function download_url($url, $path='') {
                $ch = curl_init();
                curl_setopt($ch, CURLOPT_URL, $url);
                curl_setopt($ch, CURLOPT_FRESH_CONNECT, true);
                curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
                curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 15);
                curl_setopt($ch, CURLOPT_TIMEOUT, 45);
                curl_setopt($ch, CURLOPT_ENCODING, "");
                curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
                curl_setopt($ch, CURLOPT_REFERER, "");
                curl_setopt($ch, CURLOPT_FAILONERROR, true);
                $out = curl_exec($ch) ?: false;
                curl_close($ch);
                if ($path && $out) file_put_contents($path,$out); elseif ($path) @unlink($path);
                return $out;
        }

        public function listDir($root, $ext=null) {
                $iter = new RecursiveIteratorIterator(
                                                new RecursiveDirectoryIterator($root,
                                                RecursiveDirectoryIterator::SKIP_DOTS),
                                                RecursiveIteratorIterator::SELF_FIRST,
                                                RecursiveIteratorIterator::CATCH_GET_CHILD);
                $paths = [];
                foreach ($iter as $path => $fileinfo) {
                        $fext = $fileinfo->getExtension();
                        if ($ext && $ext != $fext) continue;
                        if (substr(basename($path),0,1) == ".")  continue;
                        if ($fileinfo->isFile()) $paths[] = ['path' => $path, 'prefix' => basename(dirname($path)), 'name' => $fileinfo->getBasename(".$fext")];
                }
                return $paths;
        }

        public function getTemplates($type) {
                global $dockerManPaths;
                $tmpls = $dirs = [];
                switch ($type) {
                case 'all':
                        $dirs[] = $dockerManPaths['templates-user'];
                        $dirs[] = $dockerManPaths['templates-usb'];
                        break;
                case 'user':
                        $dirs[] = $dockerManPaths['templates-user'];
                        break;
                case 'default':
                        $dirs[] = $dockerManPaths['templates-usb'];
                        break;
                default:
                        $dirs[] = $type;
                }
                foreach ($dirs as $dir) {
                        if (!is_dir($dir)) @mkdir($dir, 0755, true);
                        $tmpls = array_merge($tmpls, $this->listDir($dir, 'xml'));
                }
                array_multisort(array_column($tmpls,'name'), SORT_NATURAL|SORT_FLAG_CASE, $tmpls);
                return $tmpls;
        }

        public function downloadTemplates($Dest=null, $Urls=null) {
                global $dockerManPaths;
                $Dest = $Dest ?: $dockerManPaths['templates-usb'];
                $Urls = $Urls ?: $dockerManPaths['template-repos'];
                $repotemplates = $output = [];
                $tmp_dir = '/tmp/tmp-'.mt_rand();
                if (!file_exists($dockerManPaths['template-repos'])) {
                        @mkdir(dirname($dockerManPaths['template-repos']), 0777, true);
                        @file_put_contents($dockerManPaths['template-repos'], 'https://github.com/limetech/docker-templates');
                }
                $urls = @file($Urls, FILE_IGNORE_NEW_LINES);
                if (!is_array($urls)) return false;
                //$this->debug("\nURLs:\n   ".implode("\n   ", $urls));
                $github_api_regexes = [
                        '%/.*github.com/([^/]*)/([^/]*)/tree/([^/]*)/(.*)$%i',
                        '%/.*github.com/([^/]*)/([^/]*)/tree/([^/]*)$%i',
                        '%/.*github.com/([^/]*)/(.*).git%i',
                        '%/.*github.com/([^/]*)/(.*)%i'
                ];
                foreach ($urls as $url) {
                        $github_api = ['url' => ''];
                        foreach ($github_api_regexes as $api_regex) {
                                if (preg_match($api_regex, $url, $matches)) {
                                        $github_api['user']   = $matches[1] ?? '';
                                        $github_api['repo']   = $matches[2] ?? '';
                                        $github_api['branch'] = $matches[3] ?? 'master';
                                        $github_api['path']   = $matches[4] ?? '';
                                        $github_api['url']    = sprintf('https://github.com/%s/%s/archive/%s.tar.gz', $github_api['user'], $github_api['repo'], $github_api['branch']);
                                        break;
                                }
                        }
                        // if after above we don't have a valid url, check for GitLab
                        if (empty($github_api['url'])) {
                                $source = $this->download_url($url);
                                // the following should always exist for GitLab Community Edition or GitLab Enterprise Edition
                                if (preg_match("/<meta content='GitLab (Community|Enterprise) Edition' name='description'>/", $source) > 0) {
                                        $parse = parse_url($url);
                                        $custom_api_regexes = [
                                                '%/'.$parse['host'].'/([^/]*)/([^/]*)/tree/([^/]*)/(.*)$%i',
                                                '%/'.$parse['host'].'/([^/]*)/([^/]*)/tree/([^/]*)$%i',
                                                '%/'.$parse['host'].'/([^/]*)/(.*).git%i',
                                                '%/'.$parse['host'].'/([^/]*)/(.*)%i',
                                        ];
                                        foreach ($custom_api_regexes as $api_regex) {
                                                if (preg_match($api_regex, $url, $matches)) {
                                                        $github_api['user']   = $matches[1] ?? '';
                                                        $github_api['repo']   = $matches[2] ?? '';
                                                        $github_api['branch'] = $matches[3] ?? 'master';
                                                        $github_api['path']   = $matches[4] ?? '';
                                                        $github_api['url']    = sprintf('https://'.$parse['host'].'/%s/%s/repository/archive.tar.gz?ref=%s', $github_api['user'], $github_api['repo'], $github_api['branch']);
                                                        break;
                                                }
                                        }
                                }
                        }
                        if (empty($github_api['url'])) {
                                //$this->debug("\n Cannot parse URL ".$url." for Templates.");
                                continue;
                        }
                        if ($this->download_url($github_api['url'], "$tmp_dir.tar.gz") === false) {
                                //$this->debug("\n Download ".$github_api['url']." has failed.");
                                @unlink("$tmp_dir.tar.gz");
                                return null;
                        } else {
                                @mkdir($tmp_dir, 0777, true);
                                shell_exec("tar -zxf $tmp_dir.tar.gz --strip=1 -C $tmp_dir/ 2>&1");
                                unlink("$tmp_dir.tar.gz");
                        }
                        $tmplsStor = [];
                        //$this->debug("\n Templates found in ".$github_api['url']);
                        foreach ($this->getTemplates($tmp_dir) as $template) {
                                $storPath = sprintf('%s/%s', $Dest, str_replace($tmp_dir.'/', '', $template['path']));
                                $tmplsStor[] = $storPath;
                                if (!is_dir(dirname($storPath))) @mkdir(dirname($storPath), 0777, true);
                                if (is_file($storPath)) {
                                        if (sha1_file($template['path']) === sha1_file($storPath)) {
                                                //$this->debug("   Skipped: ".$template['prefix'].'/'.$template['name']);
                                                continue;
                                        } else {
                                                @copy($template['path'], $storPath);
                                                //$this->debug("   Updated: ".$template['prefix'].'/'.$template['name']);
                                        }
                                } else {
                                        @copy($template['path'], $storPath);
                                        //$this->debug("   Added: ".$template['prefix'].'/'.$template['name']);
                                }
                        }
                        $repotemplates = array_merge($repotemplates, $tmplsStor);
                        $output[$url] = $tmplsStor;
                        $this->removeDir($tmp_dir);
                }
                // Delete any templates not in the repos
                foreach ($this->listDir($Dest, 'xml') as $arrLocalTemplate) {
                        if (!in_array($arrLocalTemplate['path'], $repotemplates)) {
                                unlink($arrLocalTemplate['path']);
                                //$this->debug("   Removed: ".$arrLocalTemplate['prefix'].'/'.$arrLocalTemplate['name']."\n");
                                // Any other files left in this template folder? if not delete the folder too
                                $files = array_diff(scandir(dirname($arrLocalTemplate['path'])), ['.', '..']);
                                if (empty($files)) {
                                        rmdir(dirname($arrLocalTemplate['path']));
                                        //$this->debug("   Removed: ".$arrLocalTemplate['prefix']);
                                }
                        }
                }
                return $output;
        }

        public function getTemplateValue($Repository, $field, $scope='all',$name='') {
                foreach ($this->getTemplates($scope) as $file) {
                        $doc = new DOMDocument();
                        $doc->load($file['path']);
                        if ($name) {
                                if (@$doc->getElementsByTagName('Name')->item(0)->nodeValue !== $name) continue;
                        }
                        $TemplateRepository = DockerUtil::ensureImageTag($doc->getElementsByTagName('Repository')->item(0)->nodeValue??'');
                        if ($TemplateRepository && $TemplateRepository==$Repository) {
                                $TemplateField = $doc->getElementsByTagName($field)->item(0)->nodeValue??'';
                                return trim($TemplateField);
                        }
                }
                return null;
        }

        public function getUserTemplate($Container) {
                foreach ($this->getTemplates('user') as $file) {
                        $doc = new DOMDocument('1.0', 'utf-8');
                        $doc->load($file['path']);
                        $Name = $doc->getElementsByTagName('Name')->item(0)->nodeValue??'';
                        if ($Name==$Container) return $file['path'];
                }
                return false;
        }

        private function getControlURL(&$ct, $myIP, $WebUI) {
                $port = &$ct['Ports'][0];
                $myIP = $myIP ?: $this->getTemplateValue($ct['Image'],'MyIP') ?: (_var($ct,'NetworkMode')=='host'||_var($port,'NAT') ? DockerUtil::host() : (_var($port,'IP') ?: DockerUtil::myIP($ct['Name'])));
                // Get the WebUI address from the templates as a fallback
                $WebUI = preg_replace("%\[IP\]%", $myIP, $WebUI ?? $this->getTemplateValue($ct['Image'], 'WebUI'));
                if (preg_match("%\[PORT:(\d+)\]%", $WebUI, $matches)) {
                        $ConfigPort = $matches[1] ?? '';
                        foreach ($ct['Ports'] as $port) {
                                if (_var($port,'NAT') && _var($port,'PrivatePort')==$ConfigPort) {$ConfigPort = _var($port,'PublicPort'); break;}
                        }
                        $WebUI = preg_replace("%\[PORT:\d+\]%", $ConfigPort, $WebUI);
                }
                return $WebUI;
        }

        private function getTailscaleJson($name) {
                $TS_raw = [];
                exec("docker exec -i ".$name." /bin/sh -c \"tailscale status --peers=false --json\" 2>/dev/null", $TS_raw);
                if (!empty($TS_raw)) {
                        $TS_raw = implode("\n", $TS_raw);
                        return json_decode($TS_raw, true);
                }
                return '';
        }

        public function getAllInfo($reload=false,$com=true,$communityApplications=false) {
                global $driver, $dockerManPaths;
                $DockerClient = new DockerClient();
                $DockerUpdate = new DockerUpdate();
                $host = DockerUtil::host();
                //$DockerUpdate->verbose = $this->verbose;
                $info = DockerUtil::loadJSON($dockerManPaths['webui-info']);
                $autoStart = array_map('var_split', @file($dockerManPaths['autostart-file'],FILE_IGNORE_NEW_LINES) ?: []);
                foreach ($DockerClient->getDockerContainers() as $ct) {
                        $name = $ct['Name'];
                        $image = $ct['Image'];
                        $tmp = &$info[$name] ?? [];
                        $tmp['running'] = $ct['Running'];
                        $tmp['paused'] = $ct['Paused'];
                        $tmp['autostart'] = in_array($name,$autoStart);
                        $tmp['cpuset'] = $ct['CPUset'];
                        $tmp['url'] = $ct['Url'] ?? $tmp['url'] ?? '';
                        // read docker label for WebUI & Icon
                        if (isset($ct['Icon'])) $tmp['icon'] = $ct['Icon'];
                        if (isset($ct['Shell'])) $tmp['shell'] = $ct['Shell'];
                        if (!$communityApplications) {
                                if (!is_file($tmp['icon']) || $reload) $tmp['icon'] = $this->getIcon($image,$name,$tmp['icon']);
                        }
                        if ($ct['Running']) {
                                $port = &$ct['Ports'][0];
                                $webui = $tmp['url'] ?? $this->getTemplateValue($ct['Image'], 'WebUI');
                                if (strlen($webui) > 0 && !preg_match("%\[(IP|PORT:(\d+))\]%", $webui)) {
                                        // non-templated webui, user specified
                                        $tmp['url'] = $webui;
                                } else {
                                        if ($ct['NetworkMode']=='host') {
                                                $ip = $host;
                                        } elseif (isset($driver[$ct['NetworkMode']]) && ($driver[$ct['NetworkMode']] == 'ipvlan' || $driver[$ct['NetworkMode']] == 'macvlan')) {
                                                $ip = reset($ct['Networks'])['IPAddress'];
                                        } elseif (!is_null(_var($port,'PublicPort'))) {
                                                $ip = $host;
                                        } else {
                                                $ip = _var($port,'IP');
                                        }
                                        $tmp['url'] = $ip ? (strpos($tmp['url'],$ip)!==false ? $tmp['url'] : $this->getControlURL($ct, $ip, $tmp['url'])) : $tmp['url'];
                                        if (strpos($ct['NetworkMode'], 'container:') === 0)
                                          $tmp['url'] = '';
                                }
                                // Check if webui & ct TSurl is set, if set construct WebUI URL on Docker page
                                $tmp['TSurl'] = '';
                                if (!empty($webui) && !empty($ct['TSUrl'])) {
                                        $TS_no_peers = $this->getTailscaleJson($name);
                                        if (!empty($TS_no_peers['CurrentTailnet']) && !empty($TS_no_peers['CurrentTailnet']['MagicDNSEnabled'])) {
                                                $TS_container = $TS_no_peers['Self'];
                                                $TS_DNSName = _var($TS_container,'DNSName','');
                                                $TS_HostNameActual = substr($TS_DNSName, 0, strpos($TS_DNSName, '.'));
                                                // Check if serve or funnel are enabled by checking for [hostname] and replace string with TS_DNSName
                                                if (strpos($ct['TSUrl'], '[hostname]') !== false && isset($TS_DNSName)) {
                                                        $tmp['TSurl'] = str_replace("[hostname][magicdns]", rtrim($TS_DNSName, '.'), $ct['TSUrl']);
                                                        $tmp['TSurl'] = preg_replace('/\[IP\]/', rtrim($TS_DNSName, '.'), $tmp['TSurl']);
                                                        $tmp['TSurl'] = preg_replace('/\[PORT:(\d{1,5})\]/', '443', $tmp['TSurl']);
                                                // Check if serve is disabled, construct url with port, path and query if present and replace [noserve] with url
                                                } elseif (strpos($ct['TSUrl'], '[noserve]') !== false && isset($TS_container['TailscaleIPs'])) {
                                                        $ipv4 = '';
                                                        foreach ($TS_container['TailscaleIPs'] as $ip) {
                                                                if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
                                                                        $ipv4 = $ip;
                                                                        break;
                                                                }
                                                        }
                                                        if (!empty($ipv4)) {
                                                                $webui_url = isset($webui) ? parse_url($webui) : '';
                                                                $webui_port = (preg_match('/\[PORT:(\d+)\]/', $webui, $matches)) ? ':' . $matches[1] : '';
                                                                $webui_path = $webui_url['path'] ?? '';
                                                                $webui_query = isset($webui_url['query']) ? '?' . $webui_url['query'] : '';
                                                                $webui_query = preg_replace('/\[IP\]/', $ipv4, $webui_query);
                                                                $webui_query = preg_replace('/\[PORT:(\d{1,5})\]/', ltrim($webui_port, ':'), $webui_query);
                                                                $tmp['TSurl'] = 'http://' . $ipv4 . $webui_port . $webui_path . $webui_query;
                                                        }
                                                // Check if TailscaleWebUI in the xml is custom and display instead
                                                } elseif (strpos($ct['TSUrl'], '[hostname]') === false && strpos($ct['TSUrl'], '[noserve]') === false) {
                                                        $tmp['TSurl'] = $ct['TSUrl'];
                                                }
                                        }
                                }
                                if ( ($tmp['shell'] ?? false) == false )
                                        $tmp['shell'] = $this->getTemplateValue($image, 'Shell');
                        }
                        $tmp['registry'] = $tmp['registry'] ?? $this->getTemplateValue($image, 'Registry');
                        $tmp['Support'] = $tmp['Support'] ?? $this->getTemplateValue($image, 'Support');
                        $tmp['Project'] = $tmp['Project'] ?? $this->getTemplateValue($image, 'Project');
                        $tmp['DonateLink'] = $tmp['DonateLink'] ?? $this->getTemplateValue($image, 'DonateLink');
                        $tmp['ReadMe'] = $tmp['ReadMe'] ?? $this->getTemplateValue($image, 'ReadMe');
                        if (!$com) $tmp['updated'] = 'undef';
                        if ($ct['Manager'] !== 'dockerman') {
                                $tmp['template'] = null;
                                $tmp['updated'] = null;
                        } else if (empty($tmp['template']) || $reload) {
                                $tmp['template'] = $this->getUserTemplate($name);
                                if ($reload) $DockerUpdate->updateUserTemplate($name);
                                if (empty($tmp['updated']) || $reload) {
                                        if ($reload) $DockerUpdate->reloadUpdateStatus($image);
                                        $tmp['updated'] = var_export($DockerUpdate->getUpdateStatus($image),true);
                                }
                        }
                        //$this->debug("\n$name");
                        //foreach ($tmp as $c => $d) $this->debug(sprintf('   %-10s: %s', $c, $d));
                }
                DockerUtil::saveJSON($dockerManPaths['webui-info'], $info);
                return $info;
        }

        public function getIcon($Repository,$contName,$tmpIconUrl='') {
                global $docroot, $dockerManPaths;
                $imgUrl = $this->getTemplateValue($Repository, 'Icon','all',$contName);
                if (!$imgUrl) $imgUrl = $tmpIconUrl;
                if (!$imgUrl || trim($imgUrl) == "/plugins/dynamix.docker.manager/images/question.png") return '';

                $iconRAM = sprintf('%s/%s-%s.png', $dockerManPaths['images-ram'], $contName, 'icon');
                $icon    = sprintf('%s/%s-%s.png', $dockerManPaths['images'], $contName, 'icon');

                if (!is_dir(dirname($iconRAM))) mkdir(dirname($iconRAM), 0755, true);
                if (!is_dir(dirname($icon))) mkdir(dirname($icon), 0755, true);

                if (!is_file($iconRAM)) {
                        if (!is_file($icon)) $this->download_url($imgUrl, $icon);
                        @copy($icon, $iconRAM);
                }
                if (!is_file($icon) && is_file($iconRAM)) {
                        @copy($iconRAM,$icon);
                }
                if (!is_file($iconRAM)) {
                        my_logger("$contName: Could not download icon $imgUrl");
                }

                return (is_file($iconRAM)) ? str_replace($docroot, '', $iconRAM) : '';
        }
}

####################################
##       DOCKERUPDATE CLASS       ##
####################################

class DockerUpdate{
        public $verbose = false;

        private function debug($m) {
                if ($this->verbose) echo $m."\n";
        }

        private function xml_encode($string) {
                return htmlspecialchars($string, ENT_XML1, 'UTF-8');
        }

        private function xml_decode($string) {
                return strval(html_entity_decode($string, ENT_XML1, 'UTF-8'));
        }

        public function download_url($url, $path='') {
                $ch = curl_init();
                curl_setopt($ch, CURLOPT_URL, $url);
                curl_setopt($ch, CURLOPT_FRESH_CONNECT, true);
                curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
                curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 15);
                curl_setopt($ch, CURLOPT_TIMEOUT, 45);
                curl_setopt($ch, CURLOPT_ENCODING, "");
                curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
                curl_setopt($ch, CURLOPT_REFERER, "");
                $out = curl_exec($ch) ?: false;
                curl_close($ch);
                if ($path && $out) file_put_contents($path,$out); elseif ($path) @unlink($path);
                return $out;
        }

        public function download_url_and_headers($url, $headers=[], $path='') {
                $ch = curl_init();
                curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
                curl_setopt($ch, CURLOPT_URL, $url);
                curl_setopt($ch, CURLOPT_FRESH_CONNECT, true);
                curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
                curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 15);
                curl_setopt($ch, CURLOPT_TIMEOUT, 45);
                curl_setopt($ch, CURLOPT_ENCODING, "");
                curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
                curl_setopt($ch, CURLOPT_REFERER, "");
                $out = curl_exec($ch) ?: false;
                curl_close($ch);
                if ($path && $out) file_put_contents($path,$out); elseif ($path) @unlink($path);
                return $out;
        }

        // DEPRECATED: Only used for Docker Index V1 type update checks
        public function getRemoteVersion($image) {
                extract(DockerUtil::parseImageTag($image));
                $apiUrl = sprintf('http://index.docker.io/v1/repositories/%s/tags/%s', $strRepo, $strTag);
                //$this->debug("API URL: $apiUrl");
                $apiContent = $this->download_url($apiUrl);
                return ($apiContent===false) ? null : substr(json_decode($apiContent, true)[0]['id'], 0, 8);
        }

        public function getRemoteVersionV2($image) {
                extract(DockerUtil::parseImageTag($image));
                /*
                 * Step 1: Check whether or not the image is in a private registry, get corresponding auth data and generate manifest url
                 */
                $DockerClient = new DockerClient();
                $registryAuth = $DockerClient->getRegistryAuth($image);
                $manifestURL = sprintf('%s%s%s/manifests/%s', $registryAuth['apiUrl'], $registryAuth['repository'], $registryAuth['imageName'], $registryAuth['imageTag']);
                //$this->debug('Manifest URL: '.$manifestURL);
                $ch = getCurlHandle($manifestURL, 'HEAD');
                $reply = curl_exec($ch);
                if (curl_errno($ch) !== 0) {
                        //$this->debug('Error: curl error getting manifest: '.curl_error($ch));
                        return null;
                }
                /*
                 * Step 2: Get www-authenticate header from manifest url to generate token or basic auth
                 */
                $header = ['Accept: application/vnd.docker.distribution.manifest.list.v2+json,application/vnd.docker.distribution.manifest.v2+json,application/vnd.oci.image.index.v1+json'];
                $digest_ch = getCurlHandle($manifestURL, 'HEAD', $header);
                preg_match('@www-authenticate:\s*Bearer\s*(.*)@i', $reply, $matches);
                if (!empty($matches[1])) {
                        $strArgs = explode(',', $matches[1]);
                        $args = [];
                        foreach ($strArgs as $arg) {
                                $arg = explode('=', $arg);
                                $args[$arg[0]] = trim($arg[1], "\" \r\n");
                        }
                        if (empty($args['realm']) || empty($args['service']) || empty($args['scope'])) return null;
                        $url = $args['realm'].'?service='.urlencode($args['service']).'&scope='.urlencode($args['scope']);
                        //$this->debug('Token URL: '.$url);
                        /**
                         * Step 2a: Get token from API and authenticate via username / password if in private registry and auth data was found
                         */
                        $ch = getCurlHandle($url);
                        if (!empty($registryAuth['password'])) {
                                curl_setopt($ch, CURLOPT_USERPWD, $registryAuth['username'].':'.$registryAuth['password']);
                        }
                        $reply = curl_exec($ch);
                        if (curl_errno($ch) !== 0) {
                                //$this->debug('Error: curl error getting token: '.curl_error($ch));
                                return null;
                        }
                        $reply_json = json_decode($reply, true);
                        if (!$reply_json || empty($reply_json['token'])) {
                                //$this->debug('Error: Token response was empty or missing token');
                                return null;
                        }
                        $token = $reply_json['token'];
                        array_push($header, 'Authorization: Bearer '.$token);
                }
                if (preg_match('@www-authenticate:\s*Basic\s*@i', $reply)) {
                        if (!empty($registryAuth['password'])) curl_setopt($digest_ch, CURLOPT_USERPWD, $registryAuth['username'].':'.$registryAuth['password']);
                        else return null;
                }
                /**
                 * Step 3: Get Docker-Content-Digest header from manifest file
                 */
                curl_setopt($digest_ch, CURLOPT_HTTPHEADER, $header);
                $reply = curl_exec($digest_ch);
                if (curl_errno($ch) !== 0) {
                        //$this->debug('Error: curl error getting manifest: '.curl_error($ch));
                        return null;
                }
                preg_match('@Docker-Content-Digest:\s*(.*)@i', $reply, $matches);
                if (empty($matches[1])) {
                        //$this->debug('Error: Docker-Content-Digest header is empty or missing');
                        return null;
                }
                $digest = trim($matches[1]);
                //$this->debug('Remote Digest: '.$digest);
                return $digest;
        }

        // DEPRECATED: Only used for Docker Index V1 type update checks
        public function getLocalVersion($image) {
                $DockerClient = new DockerClient();
                return substr($DockerClient->getImageID($image), 0, 8);
        }

        public function getUpdateStatus($image) {
                global $dockerManPaths;
                $image = DockerUtil::ensureImageTag($image);
                $updateStatus = DockerUtil::loadJSON($dockerManPaths['update-status']);
                if (isset($updateStatus[$image])) {
                        if (isset($updateStatus[$image]['status']) && $updateStatus[$image]['status']=='undef') return null;
                        if ($updateStatus[$image]['local'] || $updateStatus[$image]['remote']) return ($updateStatus[$image]['local']==$updateStatus[$image]['remote']);
                }
                return null;
        }

        public function reloadUpdateStatus($image=null) {
                global $dockerManPaths;
                $DockerClient = new DockerClient();
                $updateStatus = DockerUtil::loadJSON($dockerManPaths['update-status']);
                $images = $image ? [DockerUtil::ensureImageTag($image)] : array_map(function($ar){return $ar['Tags'][0]??'';}, $DockerClient->getDockerImages());
                foreach ($images as $img) {
                        $localVersion = null;
                        if (!empty($updateStatus[$img]) && array_key_exists('local', $updateStatus[$img])) {
                                $localVersion = $updateStatus[$img]['local'];
                        }
                        if ($localVersion === null) {
                                $localVersion = $this->inspectLocalVersion($img);
                        }
                        $remoteVersion = $this->getRemoteVersionV2($img);
                        $status = ($localVersion && $remoteVersion) ? (($remoteVersion == $localVersion) ? 'true' : 'false') : 'undef';
                        $updateStatus[$img] = ['local' => $localVersion, 'remote' => $remoteVersion, 'status' => $status];
                        //$this->debug("Update status: Image='$img', Local='$localVersion', Remote='$remoteVersion', Status='$status'");
                }
                DockerUtil::saveJSON($dockerManPaths['update-status'], $updateStatus);
        }

        public function inspectLocalVersion($image) {
                $DockerClient = new DockerClient();
                $inspect      = $DockerClient->getDockerJSON('/images/'.$image.'/json');
                if (empty($inspect['RepoDigests'])) return null;

                $shaPos = strpos($inspect['RepoDigests'][0], '@sha256:');
                if ($shaPos === false) return null;

                return substr($inspect['RepoDigests'][0], $shaPos + 1);
        }

        public function setUpdateStatus($image, $version) {
                global $dockerManPaths;
                $image = DockerUtil::ensureImageTag($image);
                $updateStatus = DockerUtil::loadJSON($dockerManPaths['update-status']);
                $updateStatus[$image] = ['local' => $version, 'remote' => $version, 'status' => 'true'];
                //$this->debug("Update status: Image='$image', Local='$version', Remote='$version', Status='true'");
                DockerUtil::saveJSON($dockerManPaths['update-status'], $updateStatus);
        }

        public function updateUserTemplate($Container) {
                // Don't update templates, but leave code in place for future reference
                return;

                $changed = false;
                $DockerTemplates = new DockerTemplates();
                $validElements = ['Support', 'Overview', 'Category', 'Project', 'Icon', 'ReadMe'];
                $validAttributes = ['Name', 'Default', 'Description', 'Display', 'Required', 'Mask'];
                // Get user template file and abort if fail
                if (!$file = $DockerTemplates->getUserTemplate($Container)) {
                        //$this->debug("User template for container '$Container' not found, aborting.");
                        return null;
                }
                // Load user template XML, verify if it's valid and abort if doesn't have TemplateURL element
                $template = simplexml_load_file($file);
                if (empty($template->TemplateURL)) {
                        //$this->debug("Template doesn't have TemplateURL element, aborting.");
                        return null;
                }
                // Load a user template DOM for import remote template new Config
                $dom_local = dom_import_simplexml($template);
                // Try to download the remote template and abort if it fail.
                if (!$dl = $this->download_url($this->xml_decode($template->TemplateURL))) {
                        //$this->debug("Download of remote template failed, aborting.");
                        return null;
                }
                // Try to load the downloaded template and abort if fail.
                if (!$remote_template = @simplexml_load_string($dl)) {
                        //$this->debug("The downloaded template is not a valid XML file, aborting.");
                        return null;
                }
                // Loop through remote template elements and compare them to local ones
                foreach ($remote_template->children() as $name => $remote_element) {
                        $name = $this->xml_decode($name);
                        // Compare through validElements
                        if ($name != 'Config' && in_array($name, $validElements)) {
                                $local_element = $template->xpath("//$name")[0];
                                $rvalue  = $this->xml_decode($remote_element);
                                $value   = $this->xml_decode($local_element);
                                // Values changed, updating.
                                if ($value != $rvalue) {
                                        $local_element[0] = $this->xml_encode($rvalue);
                                        //$this->debug("Updating $name from [$value] to [$rvalue]");
                                        $changed = true;
                                }
                        // Compare atributes on Config if they are in the validAttributes list
                        } elseif ($name == 'Config') {
                                $type   = $this->xml_decode($remote_element['Type']);
                                $target = $this->xml_decode($remote_element['Target']);
                                if ($type == 'Port') {
                                        $mode = $this->xml_decode($remote_element['Mode']);
                                        $local_element = $template->xpath("//Config[@Type='$type'][@Target='$target'][@Mode='$mode']")[0];
                                } else {
                                        $local_element = $template->xpath("//Config[@Type='$type'][@Target='$target']")[0];
                                }
                                // If the local template already have the pertinent Config element, loop through it's attributes and update those on validAttributes
                                if (!empty($local_element)) {
                                        foreach ($remote_element->attributes() as $key => $value) {
                                                $rvalue  = $this->xml_decode($value);
                                                $value = $this->xml_decode($local_element[$key]);
                                                // Values changed, updating.
                                                // Leave pre-existing value if description is simply Container {type}: xxx
                                                if ($value != $rvalue && in_array($key, $validAttributes)) {
                                                        if ( ! ($key == "Description" && preg_match("/^(Container Path:|Container Port:|Container Label:|Container Variable:|Container Device:)/",$rvalue) ) ) {
                                                                //$this->debug("Updating $type '$target' attribute '$key' from [$value] to [$rvalue]");
                                                                $local_element[$key] = $this->xml_encode($rvalue);
                                                                $changed = true;
                                                        }
                                                }
                                        }
                                // New Config element, add it to the local template
                                } else {
                                        $dom_remote  = dom_import_simplexml($remote_element);
                                        $new_element = $dom_local->ownerDocument->importNode($dom_remote, true);
                                        $dom_local->appendChild($new_element);
                                        $changed = true;
                                }
                        }
                }
                if ($changed) {
                        // Format output and save to file if there were any commited changes
                        //$this->debug("Saving template modifications to '$file");
                        $dom = new DOMDocument('1.0');
                        $dom->preserveWhiteSpace = false;
                        $dom->formatOutput = true;
                        $dom->loadXML($template->asXML());
                        file_put_contents($file, $dom->saveXML());
                } else {
                        //$this->debug("Template is up to date.");
                }
        }
}

####################################
##       DOCKERCLIENT CLASS       ##
####################################

class DockerClient {
        private static $containersCache = null;
        private static $imagesCache = null;
        private static $codes = [
                '200' => true, // No error
                '201' => true,
                '204' => true,
                '304' => 'Container already started',
                '400' => 'Bad parameter',
                '404' => 'No such container',
                '409' => 'Image can not be deleted, in use by other container(s)',
                '500' => 'Server error'
        ];

        private function extractID($object) {
                return substr(str_replace('sha256:', '', $object), 0, 12);
        }

        private function usedBy($imageId) {
                $out = [];
                foreach ($this->getDockerContainers() as $ct) {
                        if ($ct['ImageId']==$imageId) $out[] = $ct['Name'];
                }
                return $out;
        }

        private function flushCache(&$cache) {
                $cache = null;
        }

        public function flushCaches() {
                $this->flushCache($this::$containersCache);
                $this->flushCache($this::$imagesCache);
        }

        public function humanTiming($time) {
                $time = time() - $time; // to get the time since that moment
                $tokens = [31536000 => 'year', 2592000 => 'month', 604800 => 'week', 86400 => 'day',3600 => 'hour', 60 => 'minute', 1 => 'second'];
                foreach ($tokens as $unit => $text) {
                        if ($time < $unit) continue;
                        $numberOfUnits = floor($time / $unit);
                        return $numberOfUnits.' '.$text.(($numberOfUnits==1)?'':'s').' ago';
                }
        }

        public function formatBytes($size) {
                if ($size == 0) return '0 B';
                $base = log($size) / log(1024);
                $suffix = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
                return round(pow(1024, $base - floor($base)), 0).' '.$suffix[floor($base)];
        }

        public function getDockerJSON($url, $method='GET', &$code=null, $callback=null, $unchunk=false, $headers=null) {
                $api = ''; // latest API version. See https://docs.docker.com/develop/sdk/#api-version-matrix
                $fp = stream_socket_client('unix:///var/run/docker.sock', $errno, $errstr);
                if ($fp === false) {
                        echo "Couldn't create socket: [$errno] $errstr";
                        return [];
                }
                $protocol = $unchunk ? 'HTTP/1.0' : 'HTTP/1.1';
                $out = "$method {$api}{$url} $protocol\r\nHost:127.0.0.1\r\nConnection:Close\r\n";
                if (!empty($headers)) {
                        $out .= $headers;
                }
                $out .= "\r\n";
                fwrite($fp, $out);
                // Strip headers out
                $headers = '';
                while (($line = fgets($fp)) !== false) {
                        if (strpos($line, 'HTTP/1') !== false) {$error = vsprintf('%2$s',preg_split("#\s+#", $line)); $code = $this::$codes[$error] ?? "Error code $error";}
                        //$headers .= $line;
                        if (rtrim($line)=='') break;
                }
                $data = [];
                while (($line = fgets($fp)) !== false) {
                        if (is_array($j = json_decode($line, true))) $data = array_merge($data, $j);
                        if ($callback) $callback($line);
                }
                fclose($fp);
                return $data;
        }

        public function doesContainerExist($container) {
                foreach ($this->getDockerContainers() as $ct) {
                        if ($ct['Name']==$container) return true;
                }
                return false;
        }

        public function doesImageExist($image) {
                foreach ($this->getDockerImages() as $img) {
                        if (strpos($img['Tags'][0]??'', $image)!==false) return true;
                }
                return false;
        }

        public function getInfo() {
                $info = $this->getDockerJSON("/info");
                $version = $this->getDockerJSON("/version");
                return array_merge($info, $version);
        }

        public function getContainerLog($id, $callback, $tail=null, $since=null) {
                $this->getDockerJSON("/containers/$id/logs?stderr=1&stdout=1&tail=".urlencode($tail)."&since=".urlencode($since), 'GET', $code, $callback, true);
        }

        public function getContainerDetails($id) {
                return $this->getDockerJSON("/containers/$id/json");
        }

        public function startContainer($id) {
                $this->getDockerJSON("/containers/$id/start", 'POST', $code);
                $this->flushCache($this::$containersCache);
                addRoute($id); // add route for remote WireGuard access
                return $code;
        }

        public function pauseContainer($id) {
                $this->getDockerJSON("/containers/$id/pause", 'POST', $code);
                $this->flushCache($this::$containersCache);
                return $code;
        }

        public function stopContainer($id, $t=false) {
                global $dockercfg;
                $this->getDockerJSON("/containers/$id/stop?t=".($t ?: _var($dockercfg,'DOCKER_TIMEOUT',10)), 'POST', $code);
                $this->flushCache($this::$containersCache);
                return $code;
        }

        public function resumeContainer($id) {
                $this->getDockerJSON("/containers/$id/unpause", 'POST', $code);
                $this->flushCache($this::$containersCache);
                return $code;
        }

        public function restartContainer($id) {
                $this->getDockerJSON("/containers/$id/restart", 'POST', $code);
                $this->flushCache($this::$containersCache);
                addRoute($id); // add route for remote WireGuard access
                return $code;
        }

        public function removeContainer($name, $id=false, $cache=false) {
                global $docroot, $dockerManPaths;
                $id = $id ?: $name;
                $info = DockerUtil::loadJSON($dockerManPaths['webui-info']);
                // Attempt to remove container
                $this->getDockerJSON("/containers/$id?force=1", 'DELETE', $code);
                if (isset($info[$name])) {
                        if (isset($info[$name]['icon'])) {
                                $iconRAM = $docroot.$info[$name]['icon'];
                                $iconUSB = str_replace($dockerManPaths['images-ram'], $dockerManPaths['images'], $iconRAM);
                                if ($cache>=1 && is_file($iconRAM)) unlink($iconRAM);
                                if ($cache==2 && $code===true && is_file($iconUSB)) unlink($iconUSB);
                        }
                        unset($info[$name]);
                        DockerUtil::saveJSON($dockerManPaths['webui-info'], $info);
                }
                $this->flushCaches();
                return $code;
        }

        public function pullImage($image, $callback=null) {
                $header = null;
                $registryAuth = $this->getRegistryAuth($image);
                if ($registryAuth) {
                        $header = 'X-Registry-Auth: '.base64_encode(json_encode([
                                        'username'      => $registryAuth['username'],
                                        'password'      => $registryAuth['password'],
                                        'serveraddress' => $registryAuth['apiUrl'],
                                ]))."\r\n";
                }

                $ret = $this->getDockerJSON("/images/create?fromImage=".urlencode($image), 'POST', $code, $callback, false, $header);
                $this->flushCache($this::$imagesCache);
                return $ret;
        }

        public function getRegistryAuth($image) {
                $image = DockerUtil::ensureImageTag($image);
                //$this->debug('Image: '.$image);
                preg_match('@^([^/]+\.[^/]+/)?([^/]+/)?(.+:)(.+)$@', $image, $matches);
                list(,$registry, $repository, $image, $tag) = $matches;
                $ret = [
                        'username'     => '',
                        'password'     => '',
                        'registryName' => substr($registry, 0, -1),
                        'repository' => $repository,
                        'imageName'    => substr($image, 0, -1),
                        'imageTag'     => $tag,
                        'apiUrl'       => 'https://'.$registry.'v2/',
                ];
                $configKey = $ret['registryName'];
                if (empty($registry) || $configKey == 'docker.io') {
                        $ret['apiUrl'] = 'https://registry-1.docker.io/v2/';
                }
                // Use credentials if docker.io registry is specified
                if ($configKey == 'docker.io') {
                        $configKey = 'https://index.docker.io/v1/';
                }
                $dockerConfig = '/root/.docker/config.json';
                if (!file_exists($dockerConfig)) {
                        return $ret;
                }
                $dockerConfig = json_decode(file_get_contents($dockerConfig), true);
                if (empty($dockerConfig['auths']) || empty($dockerConfig['auths'][ $configKey ])) {
                        return $ret;
                }
                [$ret['username'], $ret['password']] = array_pad(explode(':', base64_decode($dockerConfig['auths'][ $configKey ]['auth'])),2,'');

                return $ret;
        }

        public function removeImage($id) {
                global $dockerManPaths;
                $image = $this->getImageName($id);
                // Attempt to remove image
                $this->getDockerJSON("/images/$id?force=1", 'DELETE', $code);
                if ($code===true) {
                        // Purge cached image information (only if delete was successful)
                        $image = DockerUtil::ensureImageTag($image);
                        $updateStatus = DockerUtil::loadJSON($dockerManPaths['update-status']);
                        if (isset($updateStatus[$image])) {
                                unset($updateStatus[$image]);
                                DockerUtil::saveJSON($dockerManPaths['update-status'], $updateStatus);
                        }
                }
                $this->flushCache($this::$imagesCache);
                return $code;
        }

        public function getDockerContainers() {
                global $driver;
                $host = DockerUtil::host();
                // Return cached values
                if (is_array($this::$containersCache)) return $this::$containersCache;
                $this::$containersCache = [];
                foreach ($this->getDockerJSON("/containers/json?all=1") as $ct) {
                        $info = $this->getContainerDetails($ct['Id']);
                        $c = [];
                        $c['Image']       = DockerUtil::ensureImageTag($info['Config']['Image']);
                        $c['ImageId']     = $this->extractID($ct['ImageID']);
                        $c['Name']        = substr($info['Name'], 1);
                        $c['Status']      = $ct['Status'] ?: 'None';
                        $c['Running']     = $info['State']['Running'];
                        $c['Paused']      = $info['State']['Paused'];
                        $c['Cmd']         = $ct['Command'];
                        $c['Id']          = $this->extractID($ct['Id']);
                        $c['Volumes']     = $info['HostConfig']['Binds'];
                        $c['Created']     = $this->humanTiming($ct['Created']);
                        $c['NetworkMode'] = $ct['HostConfig']['NetworkMode'];
                        $c['Manager']     = $info['Config']['Labels']['net.unraid.docker.managed'] ?? false;
                        if ($c['Manager'] == 'composeman') {
                                $c['ComposeProject'] = $info['Config']['Labels']['com.docker.compose.project'];
                        }
                        [$net, $id]       = array_pad(explode(':',$c['NetworkMode']),2,'');
                        $c['CPUset']      = $info['HostConfig']['CpusetCpus'];
                        $c['BaseImage']   = $ct['Labels']['BASEIMAGE'] ?? false;
                        $c['Icon']        = $info['Config']['Labels']['net.unraid.docker.icon'] ?? false;
                        $c['Url']         = $info['Config']['Labels']['net.unraid.docker.webui'] ?? false;
                        $c['TSUrl']       = $info['Config']['Labels']['net.unraid.docker.tailscale.webui'] ?? false;
                        $c['TSHostname']  = $info['Config']['Labels']['net.unraid.docker.tailscale.hostname'] ?? false;
                        $c['Shell']       = $info['Config']['Labels']['net.unraid.docker.shell'] ?? false;
                        $c['Manager']     = $info['Config']['Labels']['net.unraid.docker.managed'] ?? false;
                        $c['Ports']       = [];
                        $c['Networks']    = [];
                        if ($id) $c['NetworkMode'] = $net.str_replace('/',':',DockerUtil::ctMap($id)?:'/???');
                        if (isset($driver[$c['NetworkMode']])) {
                                if ($driver[$c['NetworkMode']]=='bridge') {
                                        $ports = &$info['HostConfig']['PortBindings'];
                                } elseif ($driver[$c['NetworkMode']]=='host') {
                                        $c['Ports']['host'] = ['host' => ''];
                                } elseif ($driver[$c['NetworkMode']]=='ipvlan' || $driver[$c['NetworkMode']]=='macvlan') {
                                        $i = $ct['NetworkSettings']['Networks'][$c['NetworkMode']]['IPAddress'];
                                        $c['Ports']['vlan'] = ["$i" => $i];
                                } else {
                                        $ports = &$info['Config']['ExposedPorts'];
                                }
                        } else if (!$id) {
                                $c['NetworkMode'] = DockerUtil::ctMap($c['NetworkMode']);
                                $ports = &$info['Config']['ExposedPorts'];
                        }
                        foreach($ct['NetworkSettings']['Networks'] as $netName => $netVals) {
                                $i = $c['NetworkMode']=='host' ? $host : $netVals['IPAddress'];
                                $c['Networks'][$netName] = [ 'IPAddress' => $i ];
                                if ($driver[$netName]=='ipvlan' || $driver[$netName]=='macvlan') {
                                        if (!isset($c['Ports']['vlan'])) $c['Ports']['vlan'] = [];
                                        $c['Ports']['vlan']["$i"] = $i;
                                }
                        }
                        $ip = $c['NetworkMode']=='host' ? $host : $ct['NetworkSettings']['Networks'][$c['NetworkMode']]['IPAddress'] ?? null;
                        $c['Networks'][$c['NetworkMode']] = [ 'IPAddress' => $ip ];
                        $ports = (isset($ports) && is_array($ports)) ? $ports : [];
                        foreach ($ports as $port => $value) {
                                if (!isset($info['HostConfig']['PortBindings'][$port])) {
                                        continue;
                                }
                                [$PrivatePort, $Type] = array_pad(explode('/', $port),2,'');
                                $PublicPort = $info['HostConfig']['PortBindings']["$port"][0]['HostPort'] ?: null;
                                $nat = ($driver[$c['NetworkMode']]=='bridge');
                                if (array_key_exists($PrivatePort, $c['Ports']) && $Type != $c['Ports'][$PrivatePort]['Type'])
                                        $Type = $c['Ports'][$PrivatePort]['Type'] . '/' . $Type;
                                $c['Ports'][$PrivatePort] = ['IP' => $ip, 'PrivatePort' => $PrivatePort, 'PublicPort' => $PublicPort, 'NAT' => $nat, 'Type' => $Type, 'Driver' => $driver[$c['NetworkMode']]];
                        }
                        ksort($c['Ports']);
                        $this::$containersCache[] = $c;
                }
                array_multisort(array_column($this::$containersCache,'Name'), SORT_NATURAL|SORT_FLAG_CASE, $this::$containersCache);
                return $this::$containersCache;
        }

        public function getContainerID($Container) {
                foreach ($this->getDockerContainers() as $ct) {
                        if (preg_match('%'.preg_quote($Container, '%').'%', $ct['Name'])) return $ct['Id'];
                }
                return null;
        }

        public function getImageID($Image) {
                if (!strpos($Image,':')) $Image .= ':latest';
                foreach ($this->getDockerImages() as $img) {
                        foreach ($img['Tags'] as $tag) {
                                if ($Image==$tag) return $img['Id'];
                        }
                }
                return null;
        }

        public function getImageName($id) {
                foreach ($this->getDockerImages() as $img) {
                        if ($img['Id']==$id) return $img['Tags'][0]??'';
                }
                return null;
        }

        public function getDockerImages() {
                // Return cached values
                if (is_array($this::$imagesCache)) return $this::$imagesCache;
                $this::$imagesCache = [];
                foreach ($this->getDockerJSON("/images/json?all=0") as $ct) {
                        $c = [];
                        $c['Created']     = $this->humanTiming($ct['Created']);
                        $c['Id']          = $this->extractID($ct['Id']);
                        $c['ParentId']    = $this->extractID($ct['ParentId']);
                        $c['Size']        = $this->formatBytes($ct['Size']);
                        $c['VirtualSize'] = $this->formatBytes($ct['VirtualSize'] ?? null);
                        $c['Tags']        = array_map('htmlspecialchars', $ct['RepoTags'] ?? []);
                        $c['Repository']  = DockerUtil::parseImageTag($ct['RepoTags'][0]??'')['strRepo'];
                        $c['usedBy']      = $this->usedBy($c['Id']);
                        $this::$imagesCache[$c['Id']] = $c;
                }
                return $this::$imagesCache;
        }
}

##################################
##       DOCKERUTIL CLASS       ##
##################################

class DockerUtil {
        public static function ensureImageTag($image): string {
                extract(static::parseImageTag($image));
                return "$strRepo:$strTag";
        }

        public static function parseImageTag($image): array {
                if (strpos($image, 'sha256:') === 0) {
                        // sha256 was provided instead of actual repo name so truncate it for display:
                        $strRepo = substr($image, 7, 12);
                } elseif (strpos($image, '/') === false) {
                        return static::parseImageTag('library/' . $image);
                } else {
                        $parsedImage = static::splitImage($image);
                        if (!empty($parsedImage)) {
                                $strRepo = $parsedImage['strRepo'];
                                $strTag = $parsedImage['strTag'];
                        } else {
                                // Unprocessable input
                                $strRepo = $image;
                        }
                }
                // Add :latest tag to image if it's absent
                if (empty($strTag)) $strTag = 'latest';
                return array_map('trim', ['strRepo' => $strRepo, 'strTag' => $strTag]);
        }

        private static function splitImage($image): ?array {
                if (false === preg_match('@^(.+/)*([^/:]+)(:[^:/]*)*$@', $image, $newSections) || count($newSections) < 3) {
                        return null;
                } else {
                        [, $strRepo, $image, $strTag] = array_merge($newSections, ['']);
                        $strTag = str_replace(':','',$strTag??'');
                        return [
                                'strRepo' => $strRepo . $image,
                                'strTag' => $strTag,
                        ];
                }
        }

        public static function loadJSON($path) {
                $objContent = (file_exists($path)) ? json_decode(@file_get_contents($path), true) : [];
                if (empty($objContent)) $objContent = [];
                return $objContent;
        }

        public static function saveJSON($path, $content) {
                if (!is_dir(dirname($path))) mkdir(dirname($path), 0755, true);
                return file_put_contents($path, json_encode($content, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES));
        }

        public static function docker($cmd, $a=false) {
                $data = exec("docker $cmd 2>/dev/null", $array);
                return $a ? $array : $data;
        }

        public static function myIP($name, $version=4) {
                $ipaddr = $version==4 ? 'IPAddress' : 'GlobalIPv6Address';
                return rtrim(static::docker("inspect --format='{{range .NetworkSettings.Networks}}{{.$ipaddr}} {{end}}' $name"));
        }

        public static function driver() {
                $list = [];
                foreach (static::docker("network ls --format='{{.Name}}={{.Driver}}'",true) as $network) {[$net,$driver] = array_pad(explode('=',$network),2,''); $list[$net] = $driver;}
                return $list;
        }

        public static function custom() {
                return static::docker("network ls --filter driver='bridge' --filter driver='macvlan' --filter driver='ipvlan' --format='{{.Name}}' 2>/dev/null | grep -v '^bridge$'",true);
        }

        public static function network($custom) {
                $list = ['bridge'=>'', 'host'=>'', 'none'=>''];
                foreach ($custom as $net) $list[$net] = implode(', ',array_filter(static::docker("network inspect --format='{{range .IPAM.Config}}{{println .Subnet}}{{end}}' $net",true)));
                return $list;
        }

        public static function cpus() {
                exec('cat /sys/devices/system/cpu/*/topology/thread_siblings_list | sort -nu', $cpus);
                return $cpus;
        }

        public static function ctMap($ct, $type='Name') {
                return static::docker("inspect --format='{{.$type}}' $ct");
        }

        public static function port() {
                if (lan_port('br0')) return 'br0';
                if (lan_port('bond0')) return 'bond0';
                if (lan_port('eth0')) return 'eth0';
                if (lan_port('wlan0')) return 'wlan0';
                return '';
        }

        public static function host() {
                $port = static::port();
                if (!$port) return '';
                $port = lan_port($port,true)==0 && lan_port('wlan0') ? 'wlan0' : $port;
                return exec("ip -br -4 addr show $port scope global | sed -r 's/\/[0-9]+//g' | awk '{print $3;exit}'");
        }
}
?>
/usr/local/emhttp/plugins/dynamix.docker.manager/scripts/dockerconfig
<?PHP
/* Copyright 2015-2023, Lime Technology
 * Copyright 2015-2016, Guilherme Jardim, Eric Schultz, Jon Panozzo.
 * Copyright 2012-2023, Bergware International.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License version 2,
 * as published by the Free Software Foundation.
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 */
?>
<?
/* Define the path to the docker configuration file */
$cfgfile = "/boot/config/docker.cfg";

/* Define the default configuration values */
$cfg_defaults = [
        "DOCKER_ENABLED" => "no",
        "DOCKER_IMAGE_FILE" => "/mnt/user/system/docker/docker.img",
        "DOCKER_IMAGE_SIZE" => "20",
        "DOCKER_APP_CONFIG_PATH" => "/mnt/user/appdata/",
        "DOCKER_APP_UNRAID_PATH" => "",
        "DOCKER_READMORE" => "yes",
        "DOCKER_PID_LIMIT" => ""
];

/* Initialize the new configuration with the default values */
$cfg_new = $cfg_defaults;

/* Check if the configuration file exists */
if (file_exists($cfgfile)) {
        /* Parse the existing configuration file */
        $cfg_old = parse_ini_file($cfgfile);

        /* If the existing configuration is not empty, merge it with the defaults */
        if (!empty($cfg_old)) {
                /* Merge only missing keys from defaults */
                $cfg_new = array_merge($cfg_defaults, $cfg_old);

                /* If there are no changes between the new and old configurations, unset the new configuration */
                if (empty(array_diff_assoc($cfg_new, $cfg_old))) {
                        $cfg_new = null;
                }
        }
}

/* If the new configuration is set, write it to the configuration file */
if (isset($cfg_new)) {
        $tmp = '';
        foreach ($cfg_new as $key => $value) {
                $tmp .= "$key=\"$value\"\n";
        }
        file_put_contents($cfgfile, $tmp);
}
?>

Leave a Reply