diff --git a/admin_domains.php b/admin_domains.php index c295387c..408e64bc 100644 --- a/admin_domains.php +++ b/admin_domains.php @@ -636,6 +636,23 @@ if ($page == 'domains' || $page == 'overview') { 'alert_msg' => lng('domains.import_description') ]); } + } elseif ($action == 'duplicate') { + if (isset($_POST['send']) && $_POST['send'] == 'send') { + try { + Domains::getLocal($userinfo, $_POST)->duplicate(); + } catch (Exception $e) { + Response::dynamicError($e->getMessage()); + } + Response::redirectTo($filename, [ + 'page' => $page, + 'searchfield' => 'd.domain_ace', + 'searchtext' => $_POST['domain'] ?? "" + ]); + } else { + Response::redirectTo($filename, [ + 'page' => 'overview' + ]); + } } } elseif ($page == 'domainssleditor') { require_once __DIR__ . '/ssl_editor.php'; diff --git a/lib/Froxlor/Api/Commands/Domains.php b/lib/Froxlor/Api/Commands/Domains.php index ab81efd5..1b4bf6f2 100644 --- a/lib/Froxlor/Api/Commands/Domains.php +++ b/lib/Froxlor/Api/Commands/Domains.php @@ -110,7 +110,7 @@ class Domains extends ApiCommand implements ResourceEntity * * @param number $domain_id * @param bool $ssl_only - * optional, return only ssl enabled ip's, default false + * optional, return only ssl enabled ips, default false * @return array */ private function getIpsForDomain($domain_id = 0, $ssl_only = false) @@ -207,7 +207,7 @@ class Domains extends ApiCommand implements ResourceEntity * @param string $ssl_specialsettings * optional, custom webserver vhost-content which is added to the generated ssl-vhost, default empty * @param bool $include_specialsettings - * optional, whether or not to include non-ssl specialsettings in the generated ssl-vhost, default false + * optional, whether to include non-ssl specialsettings in the generated ssl-vhost, default false * @param bool $notryfiles * optional, [nginx only] do not generate the default try-files directive, default 0 (false) * @param bool $writeaccesslog @@ -216,7 +216,7 @@ class Domains extends ApiCommand implements ResourceEntity * optional, Enable writing an error-log file for this domain, default 1 (true) * @param string $documentroot * optional, specify homedir of domain by specifying a directory (relative to customer-docroot), be - * aware, if path starts with / it it considered a full path, not relative to customer-docroot. Also + * aware, if path starts with / it is considered a full path, not relative to customer-docroot. Also * specifying a URL is possible here (redirect), default empty (autogenerated) * @param bool $phpenabled * optional, whether php is enabled for this domain, default 0 (false) @@ -241,7 +241,7 @@ class Domains extends ApiCommand implements ResourceEntity * optional, do NOT set the systems default ssl ip addresses if none are given via $ssl_ipandport * parameter * @param bool $sslenabled - * optional, whether or not SSL is enabled for this domain, regardless of the assigned ssl-ips, default + * optional, whether SSL is enabled for this domain, regardless of the assigned ssl-ips, default * 1 (true) * @param bool $http2 * optional, whether to enable http/2 for this domain (requires to be enabled in the settings), default @@ -249,9 +249,9 @@ class Domains extends ApiCommand implements ResourceEntity * @param int $hsts_maxage * optional max-age value for HSTS header * @param bool $hsts_sub - * optional whether or not to add subdomains to the HSTS header + * optional whether to add subdomains to the HSTS header * @param bool $hsts_preload - * optional whether or not to preload HSTS header value + * optional whether to preload HSTS header value * @param bool $ocsp_stapling * optional whether to enable ocsp-stapling for this domain. default 0 (false), requires SSL * @param bool $honorcipherorder @@ -260,7 +260,7 @@ class Domains extends ApiCommand implements ResourceEntity * optional whether to enable or disable TLS sessiontickets (RFC 5077) for this domain. default 1 * (true), requires SSL * @param bool $override_tls - * optional whether or not to override system-tls settings like protocol, ssl-ciphers and if applicable + * optional whether to override system-tls settings like protocol, ssl-ciphers and if applicable * tls-1.3 ciphers, requires change_serversettings flag for the admin, default false * @param array $ssl_protocols * optional list of allowed/used ssl/tls protocols, see system.ssl_protocols setting, only used/required @@ -1076,7 +1076,7 @@ class Domains extends ApiCommand implements ResourceEntity * @param string $ssl_specialsettings * optional, custom webserver vhost-content which is added to the generated ssl-vhost, default empty * @param bool $include_specialsettings - * optional, whether or not to include non-ssl specialsettings in the generated ssl-vhost, default false + * optional, whether to include non-ssl specialsettings in the generated ssl-vhost, default false * @param bool $specialsettingsforsubdomains * optional, whether to apply specialsettings to all subdomains of this domain, default is read from * setting system.apply_specialsettings_default @@ -1088,7 +1088,7 @@ class Domains extends ApiCommand implements ResourceEntity * optional, Enable writing an error-log file for this domain, default 1 (true) * @param string $documentroot * optional, specify homedir of domain by specifying a directory (relative to customer-docroot), be - * aware, if path starts with / it it considered a full path, not relative to customer-docroot. Also + * aware, if path starts with / it is considered a full path, not relative to customer-docroot. Also * specifying a URL is possible here (redirect), default empty (autogenerated) * @param bool $phpenabled * optional, whether php is enabled for this domain, default 0 (false) @@ -1117,7 +1117,7 @@ class Domains extends ApiCommand implements ResourceEntity * optional, if set to true and no $ssl_ipandport value is given, the ip's get removed, otherwise, the * currently set value is used, default false * @param bool $sslenabled - * optional, whether or not SSL is enabled for this domain, regardless of the assigned ssl-ips, default + * optional, whether SSL is enabled for this domain, regardless of the assigned ssl-ips, default * 1 (true) * @param bool $http2 * optional, whether to enable http/2 for this domain (requires to be enabled in the settings), default @@ -1125,9 +1125,9 @@ class Domains extends ApiCommand implements ResourceEntity * @param int $hsts_maxage * optional max-age value for HSTS header * @param bool $hsts_sub - * optional whether or not to add subdomains to the HSTS header + * optional whether to add subdomains to the HSTS header * @param bool $hsts_preload - * optional whether or not to preload HSTS header value + * optional whether to preload HSTS header value * @param bool $ocsp_stapling * optional whether to enable ocsp-stapling for this domain. default 0 (false), requires SSL * @param bool $honorcipherorder @@ -2187,4 +2187,114 @@ class Domains extends ApiCommand implements ResourceEntity } throw new Exception("Not allowed to execute given command.", 403); } + + /** + * duplicate domain entry by either id or domainname. All parameters from Domains.add() can be used + * to overwrite source entity values if necessary. + * + * @param int $id + * optional, the domain-id + * @param string $domainname + * optional, the domainname + * @param string $domain + * required, name of the new domain to be added + * + * @access admin + * @return string json-encoded array + * @throws Exception + */ + public function duplicate() + { + if ($this->isAdmin()) { + // parameters + $id = $this->getParam('id', true, 0); + $dn_optional = $id > 0; + $domainname = $this->getParam('domainname', $dn_optional, ''); + $p_domain = $this->getParam('domain'); + + // get requested domain + $result = $this->apiCall('Domains.get', [ + 'id' => $id, + 'domainname' => $domainname, + ]); + + // clear some defaults + unset($result['domain_ace']); + unset($result['adminid']); + unset($result['documentroot']); + unset($result['registration_date']); + unset($result['termination_date']); + unset($result['zonefile']); + // clear auto-generated values + unset($result['bindserial']); + unset($result['dkim_privkey']); + unset($result['dkim_pubkey']); + // clear api-call generated fields + unset($result['domain_hascert']); + + // set correct ip/port information + $domain_ips = $result['ipsandports']; + unset($result['ipsandports']); + $result['ipandport'] = []; + $result['ssl_ipandport'] = []; + foreach ($domain_ips as $dip) { + if ($dip['ssl'] == 1) { + $result['ssl_ipandport'][] = $dip['id']; + } else { + $result['ipandport'][] = $dip['id']; + } + } + + // check whether we are changing the customer/owner + if ($this->getParam('customerid', true, 0) == 0 && $this->getParam('loginname', true, '') == '') { + $customerid = $result['customerid']; + } else { + $customer = $this->getCustomerData(); + $customerid = $customer['customerid']; + } + + // check for alias-domain and whether it belongs to the target user + if (!empty($result['aliasdomain']) && $customerid == $result['customerid']) { + // duplicate alias entry + $result['alias'] = $result['aliasdomain']; + } + unset($result['aliasdomain']); + + // validate possible fpm configs and whether the customer is allowed to use them + if ($customerid != $result['customerid']) { + $allowed_phpconfigs = json_decode($customer['allowed_phpconfigs'] ?? '[]', true); + if (empty($allowed_phpconfigs)) { + // system defaults + unset($result['phpsettingid']); + } elseif (!in_array($result['phpsettingid'], $allowed_phpconfigs)) { + // use the first customer allowed config + $result['phpsettingid'] = array_shift($allowed_phpconfigs); + } + } + + // translate serveralias values + $result['selectserveralias'] = 2; + if ((int)$result['wwwserveralias'] == 1) { + $result['selectserveralias'] = 1; + } elseif ((int)$result['iswildcarddomain'] == 1) { + $result['selectserveralias'] = 0; + } + unset($result['wwwserveralias']); + unset($result['iswildcarddomain']); + + $additional_params = $this->getParamList(); + // unset unneeded params from this call + unset($additional_params['id']); + unset($additional_params['domainname']); + unset($additional_params['domain']); + + // set new values and merge with optional add() parameters + $new_domain = array_merge($result, $additional_params); + $new_domain['domain'] = $p_domain; + + $result_new = $this->apiCall('Domains.add', $new_domain); + return $this->response($result_new); + } + throw new Exception("Not allowed to execute given command.", 403); + } } diff --git a/lib/Froxlor/UI/Callbacks/Text.php b/lib/Froxlor/UI/Callbacks/Text.php index 3edfb72e..2ec9735e 100644 --- a/lib/Froxlor/UI/Callbacks/Text.php +++ b/lib/Froxlor/UI/Callbacks/Text.php @@ -25,10 +25,13 @@ namespace Froxlor\UI\Callbacks; +use Froxlor\CurrentUser; +use Froxlor\Database\Database; use Froxlor\Froxlor; use Froxlor\PhpHelper; use Froxlor\UI\Panel\UI; use Froxlor\User; +use PDO; class Text { @@ -105,4 +108,44 @@ class Text 'body' => $body ]; } + + public static function domainDuplicateModal(array $attributes): array + { + $linker = UI::getLinker(); + $result = $attributes['fields']; + + $customers = [ + 0 => lng('panel.please_choose') + ]; + $result_customers_stmt = Database::prepare(" + SELECT `customerid`, `loginname`, `name`, `firstname`, `company` + FROM `" . TABLE_PANEL_CUSTOMERS . "` " . (CurrentUser::getField('customers_see_all') ? '' : " WHERE `adminid` = :adminid ") . " + ORDER BY COALESCE(NULLIF(`name`,''), `company`) ASC + "); + $params = []; + if (CurrentUser::getField('customers_see_all') == '0') { + $params['adminid'] = CurrentUser::getField('adminid'); + } + Database::pexecute($result_customers_stmt, $params); + + while ($row_customer = $result_customers_stmt->fetch(PDO::FETCH_ASSOC)) { + $customers[$row_customer['customerid']] = User::getCorrectFullUserDetails($row_customer) . ' (' . $row_customer['loginname'] . ')'; + } + + $domdup_data = include Froxlor::getInstallDir() . '/lib/formfields/admin/domains/formfield.domains_duplicate.php'; + + $body = UI::twig()->render(UI::validateThemeTemplate('/user/inline-form.html.twig'), [ + 'formaction' => $linker->getLink(['section' => 'domains', 'page' => 'domains', 'action' => 'duplicate']), + 'formdata' => $domdup_data['domain_duplicate'], + 'editid' => $attributes['fields']['id'], + 'nosubmit' => 0 + ]); + return [ + 'entry' => $attributes['fields']['id'], + 'id' => 'ddModal' . $attributes['fields']['id'], + 'title' => lng('admin.domain_duplicate_named', [$attributes['fields']['domain']]), + 'action' => 'duplicate', + 'body' => $body + ]; + } } diff --git a/lib/tablelisting/admin/tablelisting.domains.php b/lib/tablelisting/admin/tablelisting.domains.php index 11f8ec5e..17aa75bc 100644 --- a/lib/tablelisting/admin/tablelisting.domains.php +++ b/lib/tablelisting/admin/tablelisting.domains.php @@ -161,6 +161,11 @@ return [ 'id' => ':id' ], ], + 'duplicate' => [ + 'icon' => 'fa-solid fa-clone', + 'title' => lng('admin.domain_duplicate'), + 'modal' => [Text::class, 'domainDuplicateModal'], + ], 'logfiles' => [ 'icon' => 'fa-solid fa-file', 'title' => lng('panel.viewlogs'), diff --git a/lng/de.lng.php b/lng/de.lng.php index 6cf9fcf9..ff39aa19 100644 --- a/lng/de.lng.php +++ b/lng/de.lng.php @@ -489,6 +489,8 @@ return [ 'adminguide' => 'Admin Guide', 'userguide' => 'User Guide', 'apiguide' => 'API Guide', + 'domain_duplicate' => 'Domain duplizieren', + 'domain_duplicate_named' => '%s duplizieren', ], 'apikeys' => [ 'no_api_keys' => 'Keine API Keys gefunden', diff --git a/lng/en.lng.php b/lng/en.lng.php index 9169ec37..f92730a0 100644 --- a/lng/en.lng.php +++ b/lng/en.lng.php @@ -500,6 +500,8 @@ return [ 'adminguide' => 'Admin guide', 'userguide' => 'User guide', 'apiguide' => 'API guide', + 'domain_duplicate' => 'Duplicate domain', + 'domain_duplicate_named' => 'Duplicate %s', ], 'apcuinfo' => [ 'clearcache' => 'Clear APCu cache', diff --git a/templates/Froxlor/user/inline-form.html.twig b/templates/Froxlor/user/inline-form.html.twig index 0df2fc4d..7c92faa1 100644 --- a/templates/Froxlor/user/inline-form.html.twig +++ b/templates/Froxlor/user/inline-form.html.twig @@ -1,2 +1,2 @@ {% import "Froxlor/form/form.html.twig" as form %} -{{ form.form(formdata, formaction|default('#'), formdata.title, editid|default(''), true, idprefix|default('')) }} +{{ form.form(formdata, formaction|default('#'), formdata.title, editid|default(''), nosubmit|default(true), idprefix|default('')) }} diff --git a/tests/Domains/DomainsTest.php b/tests/Domains/DomainsTest.php index 716f18e6..48a771f2 100644 --- a/tests/Domains/DomainsTest.php +++ b/tests/Domains/DomainsTest.php @@ -385,6 +385,26 @@ class DomainsTest extends TestCase * * @depends testAdminDomainsMove */ + public function testAdminDomainsDuplicate() + { + global $admin_userdata; + $data = [ + 'domainname' => 'test.local', + 'domain' => 'test.duplicate.local', + 'description' => 'duplicated domain' + ]; + $json_result = Domains::getLocal($admin_userdata, $data)->duplicate(); + $result = json_decode($json_result, true)['data']; + $this->assertEquals('/var/customers/webs/test3/test.duplicate.local/', $result['documentroot']); + $this->assertEquals(1, $result['email_only']); + $this->assertEquals('test.duplicate.local', $result['domain']); + $this->assertEquals('duplicated domain', $result['description']); + } + + /** + * + * @depends testAdminDomainsDuplicate + */ public function testAdminDomainsDelete() { global $admin_userdata;