Files
2026-07-04 12:02:31 +02:00

2070 lines
74 KiB
HTML
Executable File

#!/usr/local/bin/php -nc/usr/local/directadmin/plugins/docroot-changer/php.ini
<?php
declare(strict_types=1);
function h(string $value): string
{
return htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
}
function parse_sh_value(string $value): string
{
$value = trim($value);
if ($value === '') {
return '';
}
if ($value[0] === "'" && substr($value, -1) === "'") {
return substr($value, 1, -1);
}
if ($value[0] === '"' && substr($value, -1) === '"') {
return stripcslashes(substr($value, 1, -1));
}
$value = preg_replace('/\s+#.*$/', '', $value);
return trim((string)$value);
}
function read_params(): array
{
$params = [];
if (!empty($_GET)) {
$params = array_merge($params, $_GET);
}
if (!empty($_POST)) {
$params = array_merge($params, $_POST);
}
$get_raw = getenv('GET') ?: getenv('get') ?: '';
if ($get_raw !== '') {
parse_str($get_raw, $parsed);
$params = array_merge($parsed, $params);
}
$qs = $_SERVER['QUERY_STRING'] ?? '';
if ($qs !== '') {
parse_str($qs, $parsed);
$params = array_merge($parsed, $params);
}
$post_raw = getenv('POST') ?: getenv('post') ?: '';
if ($post_raw !== '') {
parse_str($post_raw, $parsed);
$params = array_merge($parsed, $params);
}
return $params;
}
function normalize_lang_code(string $value): string
{
$value = strtolower(trim($value));
if ($value === '') {
return 'en';
}
if (strpos($value, '.') !== false) {
$value = strstr($value, '.', true) ?: $value;
}
if (strpos($value, '_') !== false) {
$value = strstr($value, '_', true) ?: $value;
}
if (strpos($value, '-') !== false) {
$value = strstr($value, '-', true) ?: $value;
}
return preg_match('/^[a-z]{2}$/', $value) ? $value : 'en';
}
function load_user_language(string $user): string
{
$env = getenv('LANGUAGE') ?: getenv('LANG') ?: '';
$lang = normalize_lang_code($env);
if ($lang !== 'en' || $env !== '') {
return $lang;
}
if ($user === '') {
return 'en';
}
$conf = '/usr/local/directadmin/data/users/' . $user . '/user.conf';
if (!is_readable($conf)) {
return 'en';
}
foreach (file($conf, FILE_IGNORE_NEW_LINES) ?: [] as $line) {
if (preg_match('/^(?:lang|language|locale)=(.+)$/i', trim((string)$line), $m)) {
return normalize_lang_code((string)$m[1]);
}
}
return 'en';
}
function load_user_skin(string $user): string
{
if ($user === '') {
return 'enhanced';
}
$conf = '/usr/local/directadmin/data/users/' . $user . '/user.conf';
if (!is_readable($conf)) {
return 'enhanced';
}
foreach (file($conf, FILE_IGNORE_NEW_LINES) ?: [] as $line) {
$line = trim((string)$line);
if ($line === '' || $line[0] === '#') {
continue;
}
if (preg_match('/^skin=(.+)$/i', $line, $m)) {
$skin = strtolower(trim((string)$m[1]));
if ($skin === 'evolution' || $skin === 'enhanced') {
return $skin;
}
return 'enhanced';
}
}
return 'enhanced';
}
function load_lang_map(string $lang, string $plugin_dir): array
{
$lang = normalize_lang_code($lang);
$lang_dir = $plugin_dir . '/lang';
$fallback = $lang_dir . '/en.php';
$path = $lang_dir . '/' . $lang . '.php';
$map = [];
if (is_file($path)) {
$loaded = include $path;
if (is_array($loaded)) {
$map = $loaded;
}
}
if (empty($map) && is_file($fallback)) {
$loaded = include $fallback;
if (is_array($loaded)) {
$map = $loaded;
}
}
return is_array($map) ? $map : [];
}
$lang_map = [];
function t(string $text): string
{
global $lang_map;
return $lang_map[$text] ?? $text;
}
function load_settings(string $path): array
{
$settings = [
'CUSTOM_PACKAGE_ITEM_NAME' => 'docroot_changer',
'CUSTOM_PACKAGE_ITEM_DEFAULT' => 'no',
'ENFORCE_DOMAINS_BASE' => '/home/DA_USER/domains',
'ALLOW_PICKER_CREATE_DIR' => 'yes',
'ALLOW_DOCROOT_OUTSIDE_DOMAINS_DIR' => 'no',
'DISABLE_CONFIRMATIONS' => 'false',
'FIX_OWNERSHIP' => 'true',
];
if (!is_readable($path)) {
return $settings;
}
foreach (file($path, FILE_IGNORE_NEW_LINES) ?: [] as $line) {
$line = trim((string)$line);
if ($line === '' || $line[0] === '#') {
continue;
}
if (preg_match('/^(CUSTOM_PACKAGE_ITEM_NAME|CUSTOM_PACKAGE_ITEM_DEFAULT|ENFORCE_DOMAINS_BASE|ALLOW_PICKER_CREATE_DIR|ALLOW_DOCROOT_OUTSIDE_DOMAINS_DIR|DISABLE_CONFIRMATIONS|FIX_OWNERSHIP)=(.*)$/', $line, $m)) {
$settings[$m[1]] = parse_sh_value((string)$m[2]);
}
}
return $settings;
}
function apply_da_user(string $value, string $da_user): string
{
return str_replace('DA_USER', $da_user, $value);
}
function normalize_location(string $path, string $fallback): string
{
$path = trim($path);
if ($path === '') {
$path = $fallback;
}
if ($path !== '' && $path[0] !== '/') {
$path = '/' . $path;
}
$path = preg_replace('#/+#', '/', $path) ?: $path;
return rtrim($path, '/');
}
function safe_user_key(string $value): string
{
$key = preg_replace('/[^A-Za-z0-9_.-]/', '_', $value);
return $key === '' ? 'unknown' : $key;
}
function ensure_dir(string $path, int $mode = 0700): void
{
if (!is_dir($path)) {
@mkdir($path, $mode, true);
}
}
function get_csrf_token(string $data_dir, string $da_user): string
{
$dir = rtrim($data_dir, '/') . '/csrf';
ensure_dir($dir, 0700);
$path = $dir . '/' . safe_user_key($da_user) . '.token';
if (is_readable($path)) {
$token = trim((string)file_get_contents($path));
if ($token !== '') {
return $token;
}
}
$token = bin2hex(random_bytes(32));
@file_put_contents($path, $token, LOCK_EX);
@chmod($path, 0600);
return $token;
}
function csrf_is_valid(array $params, string $token): bool
{
$sent = $params['csrf_token'] ?? '';
return is_string($sent) && $sent !== '' && $token !== '' && hash_equals($token, $sent);
}
function ajax_output(array $payload, int $status_code = 200): void
{
http_response_code($status_code);
header('Content-Type: text/plain; charset=utf-8');
$json = json_encode($payload, JSON_UNESCAPED_UNICODE);
if ($json === false) {
$json = '{"ok":false,"error":"json_encode_failed"}';
}
echo '__DOCROOT_AJAX_JSON__';
echo $json;
echo '__DOCROOT_AJAX_JSON__';
exit;
}
function load_user_conf_value(string $user, string $key): string
{
if ($user === '' || $key === '') {
return '';
}
$path = '/usr/local/directadmin/data/users/' . $user . '/user.conf';
if (!is_readable($path)) {
return '';
}
$value = '';
foreach (file($path, FILE_IGNORE_NEW_LINES) ?: [] as $line) {
$line = trim((string)$line);
if ($line === '' || $line[0] === '#') {
continue;
}
if (strpos($line, $key . '=') === 0) {
$value = substr($line, strlen($key) + 1);
}
}
return trim($value);
}
function parse_bool(string $value, bool $default = false): bool
{
$value = strtolower(trim($value));
if ($value === '') {
return $default;
}
if (in_array($value, ['1', 'yes', 'true', 'on', 'y'], true)) {
return true;
}
if (in_array($value, ['0', 'no', 'false', 'off', 'n'], true)) {
return false;
}
return $default;
}
function is_plugin_enabled_for_user(string $user, string $custom_item_name, string $default): bool
{
$value = load_user_conf_value($user, $custom_item_name);
if ($value === '') {
return parse_bool($default, false);
}
return parse_bool($value, false);
}
function is_valid_domain_name(string $domain): bool
{
if ($domain === '' || strlen($domain) > 255) {
return false;
}
return (bool)preg_match('/^(?=.{1,255}$)[A-Za-z0-9](?:[A-Za-z0-9.-]*[A-Za-z0-9])$/', $domain);
}
function normalize_domain_to_ascii(string $domain): string
{
$domain = trim($domain);
if ($domain === '') {
return '';
}
$domain = str_replace(["\u{3002}", "\u{FF0E}", "\u{FF61}"], '.', $domain);
$domain = rtrim($domain, '.');
if (preg_match('/[^\x20-\x7E]/', $domain)) {
if (!function_exists('idn_to_ascii')) {
return '';
}
$variant = defined('INTL_IDNA_VARIANT_UTS46') ? INTL_IDNA_VARIANT_UTS46 : 0;
$flags = defined('IDNA_DEFAULT') ? IDNA_DEFAULT : 0;
$ascii = @idn_to_ascii($domain, $flags, $variant);
if (!is_string($ascii) || $ascii === '') {
return '';
}
$domain = $ascii;
}
return strtolower($domain);
}
function list_user_domains(string $user): array
{
$file = '/usr/local/directadmin/data/users/' . $user . '/domains.list';
if (!is_readable($file)) {
return [];
}
$domains = [];
foreach (file($file, FILE_IGNORE_NEW_LINES) ?: [] as $line) {
$domain = trim((string)$line);
if ($domain === '' || !is_valid_domain_name($domain)) {
continue;
}
$domains[$domain] = true;
}
$list = array_keys($domains);
sort($list, SORT_NATURAL | SORT_FLAG_CASE);
return $list;
}
function path_is_within(string $path, string $base): bool
{
$path = rtrim($path, '/');
$base = rtrim($base, '/');
if ($base === '') {
return false;
}
if ($path === $base) {
return true;
}
return strpos($path . '/', $base . '/') === 0;
}
function detect_webserver(): string
{
$value = read_custombuild_option('webserver');
return $value !== '' ? $value : 'apache';
}
function read_custombuild_option(string $key): string
{
$key = trim($key);
if ($key === '') {
return '';
}
$path = '/usr/local/directadmin/custombuild/options.conf';
if (!is_readable($path)) {
return '';
}
foreach (file($path, FILE_IGNORE_NEW_LINES) ?: [] as $line) {
$line = trim((string)$line);
if (strpos($line, $key . '=') === 0) {
return strtolower(trim(substr($line, strlen($key) + 1)));
}
}
return '';
}
function is_cloudlinux_system(): bool
{
$release_files = ['/etc/redhad-release', '/etc/redhat-release'];
foreach ($release_files as $path) {
if (!is_readable($path)) {
continue;
}
$content = (string)@file_get_contents($path);
if ($content !== '' && stripos($content, 'cloudlinux') !== false) {
return true;
}
}
return false;
}
function webserver_targets(string $webserver): array
{
$webserver = strtolower(trim($webserver));
switch ($webserver) {
case 'nginx_apache':
return ['apache' => true, 'nginx' => true, 'openlitespeed' => false];
case 'nginx':
return ['apache' => false, 'nginx' => true, 'openlitespeed' => false];
case 'openlitespeed':
return ['apache' => false, 'nginx' => false, 'openlitespeed' => true];
case 'litespeed':
case 'apache':
return ['apache' => true, 'nginx' => false, 'openlitespeed' => false];
default:
return ['apache' => true, 'nginx' => false, 'openlitespeed' => false];
}
}
function custom_config_file(string $user, string $domain, string $target): string
{
$base = '/usr/local/directadmin/data/users/' . $user . '/domains/' . $domain;
if ($target === 'nginx') {
return $base . '.cust_nginx';
}
if ($target === 'openlitespeed') {
return $base . '.cust_openlitespeed';
}
return $base . '.cust_httpd';
}
function read_custom_config_value(string $path): string
{
if (!is_readable($path) || !is_file($path)) {
return '';
}
$text = (string)file_get_contents($path);
if ($text === '') {
return '';
}
if (preg_match('/\|\?DOCROOT=([^|]+)\|/', $text, $m)) {
return trim((string)$m[1]);
}
return '';
}
function default_docroot(string $user, string $domain): string
{
return '/home/' . $user . '/domains/' . $domain . '/public_html';
}
function resolve_domain_docroot_state(string $user, string $domain, array $targets): array
{
$default = default_docroot($user, $domain);
$values = [];
if (!empty($targets['apache'])) {
$value = read_custom_config_value(custom_config_file($user, $domain, 'apache'));
if ($value !== '') {
$values['apache'] = $value;
}
}
if (!empty($targets['nginx'])) {
$value = read_custom_config_value(custom_config_file($user, $domain, 'nginx'));
if ($value !== '') {
$values['nginx'] = $value;
}
}
if (!empty($targets['openlitespeed'])) {
$value = read_custom_config_value(custom_config_file($user, $domain, 'openlitespeed'));
if ($value !== '') {
$values['openlitespeed'] = $value;
} else {
$fallback = read_custom_config_value(custom_config_file($user, $domain, 'apache'));
if ($fallback !== '') {
$values['apache'] = $fallback;
}
}
}
$has_custom = !empty($values);
$effective = $has_custom ? reset($values) : $default;
$unique = array_values(array_unique(array_values($values)));
$mismatch = count($unique) > 1;
return [
'default_docroot' => $default,
'current_docroot' => is_string($effective) ? $effective : $default,
'has_custom' => $has_custom,
'mismatch' => $mismatch,
'values' => $values,
];
}
function normalize_user_input_path(string $path): string
{
$path = trim($path);
if ($path === '') {
return '';
}
if ($path[0] !== '/') {
$path = '/' . $path;
}
$path = preg_replace('#/+#', '/', $path) ?: $path;
$path = rtrim($path, '/');
return $path === '' ? '/' : $path;
}
function collapse_path(string $path): string
{
$path = normalize_user_input_path($path);
if ($path === '') {
return '';
}
$parts = explode('/', trim($path, '/'));
$stack = [];
foreach ($parts as $part) {
if ($part === '' || $part === '.') {
continue;
}
if ($part === '..') {
array_pop($stack);
continue;
}
$stack[] = $part;
}
return '/' . implode('/', $stack);
}
function validate_docroot_path(string $input, string $domains_base, string $user_home, bool $allow_outside_domains): array
{
$input = trim($input);
if ($input === '') {
return ['ok' => false, 'error' => t('Docroot musi wskazywać istniejący katalog.')];
}
if (preg_match('/[\x00-\x1F\x7F]/u', $input)) {
return ['ok' => false, 'error' => t('ścieżka zawiera nieprawidłowe znaki - proszę wprowadzić poprawną ścieżkę lub skorzystać z opcji __ICON__ w celu wyboru poprawnie ścieżki.')];
}
if (!preg_match('/^[\p{L}\p{N}\/._\- ]+$/u', $input)) {
return ['ok' => false, 'error' => t('ścieżka zawiera nieprawidłowe znaki - proszę wprowadzić poprawną ścieżkę lub skorzystać z opcji __ICON__ w celu wyboru poprawnie ścieżki.')];
}
$path = collapse_path($input);
if ($path === '') {
return ['ok' => false, 'error' => t('Docroot musi wskazywać istniejący katalog.')];
}
if (strpos($input, "\0") !== false) {
return ['ok' => false, 'error' => t('Docroot musi wskazywać istniejący katalog.')];
}
if (preg_match('#(?:^|/)\.\.(?:/|$)#', $input)) {
return ['ok' => false, 'error' => t('Ścieżka zawiera niedozwolony fragment "..".')];
}
if (!is_dir($path)) {
return ['ok' => false, 'error' => t('Docroot musi wskazywać istniejący katalog.')];
}
if (is_link($path)) {
$resolved = @realpath($path);
$target = is_string($resolved) && $resolved !== '' ? $resolved : (string)@readlink($path);
$msg = t('wprowadzona ścieżka jest dowiązaniem symbolicznym do: __TARGET__, wprowadź ścieżkę do realnego katalogu');
return ['ok' => false, 'error' => str_replace('__TARGET__', $target !== '' ? $target : '?', $msg)];
}
$path_real = realpath($path);
if (!is_string($path_real) || $path_real === '') {
return ['ok' => false, 'error' => t('Docroot musi wskazywać istniejący katalog.')];
}
if (collapse_path($path_real) !== collapse_path($path)) {
$msg = t('wprowadzona ścieżka jest dowiązaniem symbolicznym do: __TARGET__, wprowadź ścieżkę do realnego katalogu');
return ['ok' => false, 'error' => str_replace('__TARGET__', $path_real, $msg)];
}
$final_path = $path_real !== false ? $path_real : $path;
if (!path_is_within($final_path, $user_home)) {
return ['ok' => false, 'error' => t('Ścieżka musi znajdować się w katalogu użytkownika:') . ' ' . $user_home];
}
if (!$allow_outside_domains && !path_is_within($final_path, $domains_base)) {
return ['ok' => false, 'error' => t('Ścieżka musi znajdować się w katalogu:') . ' ' . $domains_base];
}
return ['ok' => true, 'path' => $final_path];
}
function detect_path_owner_group(string $path): array
{
$uid = @fileowner($path);
$gid = @filegroup($path);
$owner_name = is_int($uid) ? (string)$uid : '?';
$group_name = is_int($gid) ? (string)$gid : '?';
if (is_int($uid) && function_exists('posix_getpwuid')) {
$u = @posix_getpwuid($uid);
if (is_array($u) && isset($u['name']) && $u['name'] !== '') {
$owner_name = (string)$u['name'];
}
}
if (is_int($gid) && function_exists('posix_getgrgid')) {
$g = @posix_getgrgid($gid);
if (is_array($g) && isset($g['name']) && $g['name'] !== '') {
$group_name = (string)$g['name'];
}
}
return [
'uid' => is_int($uid) ? $uid : null,
'gid' => is_int($gid) ? $gid : null,
'owner' => $owner_name,
'group' => $group_name,
];
}
function expected_user_uid_gid(string $user): array
{
if ($user === '') {
return ['uid' => null, 'gid' => null];
}
if (!function_exists('posix_getpwnam')) {
return ['uid' => null, 'gid' => null];
}
$u = @posix_getpwnam($user);
if (!is_array($u)) {
return ['uid' => null, 'gid' => null];
}
return [
'uid' => isset($u['uid']) && is_int($u['uid']) ? $u['uid'] : null,
'gid' => isset($u['gid']) && is_int($u['gid']) ? $u['gid'] : null,
];
}
function path_owned_by_user(string $path, string $user): array
{
$current = detect_path_owner_group($path);
$expected = expected_user_uid_gid($user);
$owner_ok = false;
$group_ok = false;
if ($expected['uid'] !== null) {
$owner_ok = ($current['uid'] !== null) && ((int)$current['uid'] === (int)$expected['uid']);
} else {
$owner_ok = (string)$current['owner'] === $user;
}
if ($expected['gid'] !== null) {
$group_ok = ($current['gid'] !== null) && ((int)$current['gid'] === (int)$expected['gid']);
} else {
$group_ok = (string)$current['group'] === $user;
}
return [
'match' => $owner_ok && $group_ok,
'owner' => (string)$current['owner'],
'group' => (string)$current['group'],
];
}
function build_custom_httpd_value(?string $docroot): string
{
$docroot = $docroot === null ? '' : trim($docroot);
if ($docroot === '') {
return '';
}
$begin = '# POCZATEK ZMIANY DOCROOT';
$end = '# KONIEC ZMIANY DOCROOT';
return $begin . "\n" . '|?DOCROOT=' . $docroot . '|' . "\n" . $end . "\n";
}
function strip_plugin_docroot_block(string $content): array
{
$begin = '# POCZATEK ZMIANY DOCROOT';
$legacy_begin = '# POZATEK ZMIANY DOCROOT';
$end = '# KONIEC ZMIANY DOCROOT';
$lines = preg_split('/\r\n|\r|\n/', $content);
if (!is_array($lines)) {
return [$content, false];
}
$out = [];
$in_block = false;
$had_block = false;
foreach ($lines as $line) {
$trim = trim((string)$line);
if (!$in_block && ($trim === $begin || $trim === $legacy_begin)) {
$in_block = true;
$had_block = true;
continue;
}
if ($in_block) {
if ($trim === $end) {
$in_block = false;
}
continue;
}
$out[] = (string)$line;
}
$clean = rtrim(implode("\n", $out));
return [$clean, $had_block];
}
function merge_plugin_docroot_block(string $existing, string $plugin_block): string
{
[$without_plugin, ] = strip_plugin_docroot_block($existing);
$without_plugin = rtrim($without_plugin);
$plugin_block = rtrim($plugin_block);
if ($without_plugin === '') {
return $plugin_block . "\n";
}
return $without_plugin . "\n\n" . $plugin_block . "\n";
}
function parse_da_api_response(string $body): array
{
$normalized = str_replace(["\r\n", "\r"], "\n", $body);
$query = str_replace("\n", '&', trim($normalized));
$parsed = [];
if ($query !== '') {
parse_str($query, $parsed);
}
return is_array($parsed) ? $parsed : [];
}
function directadmin_api_request(string $path, array $post_fields): array
{
$cookie = getenv('HTTP_COOKIE') ?: '';
if ($cookie === '') {
$sid = getenv('SESSION_ID') ?: '';
if ($sid !== '') {
$cookie = 'session=' . $sid;
}
}
if ($cookie === '') {
return ['ok' => false, 'error' => 'no_session'];
}
$url = 'https://127.0.0.1:2222' . $path;
$body = '';
$transport_error = '';
if (function_exists('curl_init')) {
$ch = curl_init($url);
if ($ch === false) {
return ['ok' => false, 'error' => 'curl_init_failed'];
}
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query($post_fields, '', '&', PHP_QUERY_RFC3986),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => 8,
CURLOPT_TIMEOUT => 20,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_SSL_VERIFYHOST => false,
CURLOPT_HTTPHEADER => [
'Cookie: ' . $cookie,
'Content-Type: application/x-www-form-urlencoded',
],
]);
$resp = curl_exec($ch);
if ($resp === false) {
$transport_error = (string)curl_error($ch);
} else {
$body = (string)$resp;
}
curl_close($ch);
} else {
$opts = [
'http' => [
'method' => 'POST',
'header' => "Content-Type: application/x-www-form-urlencoded\r\nCookie: {$cookie}\r\n",
'content' => http_build_query($post_fields, '', '&', PHP_QUERY_RFC3986),
'timeout' => 20,
],
'ssl' => [
'verify_peer' => false,
'verify_peer_name' => false,
],
];
$ctx = stream_context_create($opts);
$resp = @file_get_contents($url, false, $ctx);
if ($resp === false) {
$transport_error = 'stream_request_failed';
} else {
$body = (string)$resp;
}
}
if ($body === '') {
return ['ok' => false, 'error' => $transport_error !== '' ? $transport_error : 'empty_response'];
}
$parsed = parse_da_api_response($body);
$success = true;
if (isset($parsed['error'])) {
$success = trim((string)$parsed['error']) === '0';
} elseif (stripos($body, 'error=1') !== false) {
$success = false;
}
return [
'ok' => $success,
'body' => $body,
'parsed' => $parsed,
];
}
function set_domain_custom_httpd(string $domain, string $value, bool $proxy): array
{
$path = '/CMD_API_CUSTOM_HTTPD';
if ($proxy) {
$path .= '?proxy=yes';
}
return directadmin_api_request($path, [
'domain' => $domain,
'value' => $value,
]);
}
function api_error_text(array $api_response): string
{
$error_code = (string)($api_response['error'] ?? '');
if ($error_code === 'no_session') {
return t('Brak aktywnej sesji DirectAdmin do wywołania API.');
}
if ($error_code === 'empty_response') {
return t('Brak odpowiedzi z API DirectAdmin.');
}
if (!empty($api_response['parsed']['text'])) {
return (string)$api_response['parsed']['text'];
}
if (!empty($api_response['parsed']['details'])) {
return (string)$api_response['parsed']['details'];
}
if (!empty($api_response['error'])) {
return (string)$api_response['error'];
}
return trim((string)($api_response['body'] ?? ''));
}
function apply_docroot_via_api(string $domain, ?string $docroot, array $targets): array
{
$value = build_custom_httpd_value($docroot);
$errors = [];
if (!empty($targets['apache']) || !empty($targets['openlitespeed'])) {
$resp = set_domain_custom_httpd($domain, $value, false);
if (empty($resp['ok'])) {
$errors[] = 'apache: ' . api_error_text($resp);
}
}
if (!empty($targets['nginx'])) {
$resp = set_domain_custom_httpd($domain, $value, true);
if (empty($resp['ok'])) {
$errors[] = 'nginx: ' . api_error_text($resp);
}
}
if (!empty($errors)) {
return ['ok' => false, 'error' => implode(' | ', $errors)];
}
return ['ok' => true];
}
function custom_docroot_paths(string $user, string $domain, array $targets): array
{
$base = '/usr/local/directadmin/data/users/' . $user . '/domains/' . $domain;
$paths = [];
if (!empty($targets['apache']) || !empty($targets['openlitespeed'])) {
$paths[] = $base . '.cust_httpd';
}
if (!empty($targets['nginx'])) {
$paths[] = $base . '.cust_nginx';
}
if (!empty($targets['openlitespeed'])) {
$paths[] = $base . '.cust_openlitespeed';
}
return array_values(array_unique($paths));
}
function write_custom_docroot_files(string $user, string $domain, ?string $docroot, array $targets): array
{
$paths = custom_docroot_paths($user, $domain, $targets);
$content = build_custom_httpd_value($docroot);
$clear = $docroot === null || trim($docroot) === '';
foreach ($paths as $path) {
if ($clear) {
if (!is_file($path)) {
continue;
}
$existing = @file_get_contents($path);
if ($existing === false) {
return ['ok' => false, 'error' => t('Nie udało się odczytać pliku konfiguracji webserwera: ') . $path];
}
[$stripped, $had_block] = strip_plugin_docroot_block((string)$existing);
if (!$had_block) {
continue;
}
if (trim($stripped) === '') {
if (!@unlink($path)) {
return ['ok' => false, 'error' => t('Nie udało się usunąć pliku konfiguracji webserwera: ') . $path];
}
continue;
}
$written = @file_put_contents($path, rtrim($stripped) . "\n", LOCK_EX);
if ($written === false) {
return ['ok' => false, 'error' => t('Nie udało się zapisać pliku konfiguracji webserwera: ') . $path];
}
@chmod($path, 0644);
@chown($path, 'diradmin');
@chgrp($path, 'diradmin');
continue;
}
$existing = is_file($path) ? (string)@file_get_contents($path) : '';
$merged = merge_plugin_docroot_block($existing, $content);
$written = @file_put_contents($path, $merged, LOCK_EX);
if ($written === false) {
return ['ok' => false, 'error' => t('Nie udało się zapisać pliku konfiguracji webserwera: ') . $path];
}
@chmod($path, 0644);
@chown($path, 'diradmin');
@chgrp($path, 'diradmin');
}
return ['ok' => true];
}
function queue_httpd_rewrite_for_user(string $user): array
{
$path_env = '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin';
@putenv('PATH=' . $path_env);
$user = trim($user);
if ($user === '' || !preg_match('/^[A-Za-z0-9_.-]+$/', $user)) {
return ['ok' => false, 'error' => t('Nieprawidłowa nazwa użytkownika dla zadania rewrite httpd.')];
}
$task_file = '/usr/local/directadmin/data/task.queue';
$task_line = 'action=rewrite&value=httpd&user=' . rawurlencode($user);
$written = @file_put_contents($task_file, $task_line . "\n", FILE_APPEND | LOCK_EX);
if ($written === false) {
return ['ok' => false, 'error' => t('Nie udało się dodać zadania rewrite httpd do task.queue.')];
}
$cmd = '/usr/local/directadmin/dataskq d2000';
$output = [];
$code = 1;
@exec('PATH=' . escapeshellarg($path_env) . ' ' . $cmd . ' 2>&1', $output, $code);
if ($code === 0) {
return ['ok' => true];
}
$msg = t('Nie udało się zainicjować przebudowy konfiguracji httpd przez task.queue/dataskq.');
$details = trim(implode("\n", $output));
if ($details !== '') {
$msg .= ' ' . $details;
}
return ['ok' => false, 'error' => $msg];
}
function apply_docroot_configuration(string $user, string $domain, ?string $docroot, array $targets): array
{
$written = write_custom_docroot_files($user, $domain, $docroot, $targets);
if (empty($written['ok'])) {
return $written;
}
$queued = queue_httpd_rewrite_for_user($user);
if (empty($queued['ok'])) {
return $queued;
}
return ['ok' => true];
}
function normalize_picker_base(string $base): string
{
$base = collapse_path($base);
return $base !== '' ? $base : '/';
}
function resolve_picker_path(string $requested, string $base): array
{
$base = normalize_picker_base($base);
$requested = trim($requested);
if ($requested === '') {
$requested = $base;
}
if ($requested[0] !== '/') {
$requested = $base . '/' . $requested;
}
$requested = collapse_path($requested);
if ($requested === '') {
$requested = $base;
}
if (!path_is_within($requested, $base)) {
return [$base, $base, true];
}
if (is_dir($requested)) {
return [$requested, $base, true];
}
return [$base, $base, true];
}
function picker_error(string $code): void
{
ajax_output(['ok' => false, 'error' => $code], 400);
}
function directory_is_empty(string $path): bool
{
if (!is_dir($path)) {
return false;
}
$h = @opendir($path);
if ($h === false) {
return false;
}
while (($entry = readdir($h)) !== false) {
if ($entry !== '.' && $entry !== '..') {
closedir($h);
return false;
}
}
closedir($h);
return true;
}
function handle_dir_list(array $params, string $base): void
{
[$resolved, $base_real, $ok] = resolve_picker_path((string)($params['path'] ?? ''), $base);
if (!$ok) {
picker_error('outside_base');
}
$entries = [];
$scan = @scandir($resolved);
if (!is_array($scan) && $resolved !== $base_real) {
$resolved = $base_real;
$scan = @scandir($resolved);
}
if (!is_array($scan)) {
$scan = [];
}
foreach ($scan as $name) {
if ($name === '.' || $name === '..') {
continue;
}
if ($name[0] === '.') {
continue;
}
$full = $resolved . '/' . $name;
if (is_dir($full)) {
$entries[] = [
'name' => $name,
'empty' => directory_is_empty($full),
];
}
}
usort($entries, static function (array $a, array $b): int {
return strnatcasecmp((string)$a['name'], (string)$b['name']);
});
ajax_output([
'ok' => true,
'path' => $resolved,
'base' => $base_real,
'current_empty' => directory_is_empty($resolved),
'entries' => $entries,
]);
}
function handle_dir_create(array $params, string $picker_base, string $domains_base, string $da_user, bool $allow_create): void
{
if (!$allow_create) {
picker_error('create_disabled');
}
if ($da_user === '' || !preg_match('/^[A-Za-z0-9_.-]+$/', $da_user)) {
picker_error('invalid_user');
}
$name = trim((string)($params['name'] ?? ''));
if ($name === '') {
picker_error('name_required');
}
if (!preg_match('/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/', $name)) {
picker_error('invalid_name');
}
[$parent, $base_real, $ok] = resolve_picker_path((string)($params['path'] ?? ''), $picker_base);
if (!$ok) {
picker_error('outside_base');
}
if (collapse_path($parent) === collapse_path($domains_base)) {
picker_error('main_domain_blocked');
}
$new = $parent . '/' . $name;
if (!path_is_within($new, $base_real)) {
picker_error('outside_base');
}
if (file_exists($new)) {
picker_error('exists');
}
if (!@mkdir($new, 0755, false)) {
picker_error('mkdir_failed');
}
if (!@chmod($new, 0755)) {
picker_error('chmod_failed');
}
if (!@chown($new, $da_user) || !@chgrp($new, $da_user)) {
picker_error('chown_failed');
}
ajax_output([
'ok' => true,
'created' => $new,
'path' => $parent,
'base' => $base_real,
'created_empty' => true,
]);
}
$da_user = getenv('USERNAME') ?: getenv('LOGNAME') ?: getenv('USER') ?: '';
$plugin_dir = realpath(__DIR__ . '/..') ?: '/usr/local/directadmin/plugins/docroot-changer';
$plugin_data_dir = $plugin_dir . '/data';
$settings = load_settings($plugin_dir . '/plugin-settings.conf');
$lang_code = load_user_language($da_user);
$lang_map = load_lang_map($lang_code, $plugin_dir);
$custom_item_name = trim((string)($settings['CUSTOM_PACKAGE_ITEM_NAME'] ?? 'docroot_changer'));
if ($custom_item_name === '' || !preg_match('/^[A-Za-z0-9_]+$/', $custom_item_name)) {
$custom_item_name = 'docroot_changer';
}
$custom_item_default = (string)($settings['CUSTOM_PACKAGE_ITEM_DEFAULT'] ?? 'no');
$allow_picker_create = parse_bool((string)($settings['ALLOW_PICKER_CREATE_DIR'] ?? 'yes'), true);
$allow_docroot_outside_domains = parse_bool((string)($settings['ALLOW_DOCROOT_OUTSIDE_DOMAINS_DIR'] ?? 'no'), false);
$disable_confirmations = parse_bool((string)($settings['DISABLE_CONFIRMATIONS'] ?? 'false'), false);
$fix_ownership = parse_bool((string)($settings['FIX_OWNERSHIP'] ?? 'true'), true);
$user_home = normalize_location('/home/' . $da_user, '/home/' . $da_user);
$domains_base = normalize_location(
apply_da_user((string)($settings['ENFORCE_DOMAINS_BASE'] ?? '/home/DA_USER/domains'), $da_user),
'/home/' . $da_user . '/domains'
);
$picker_base = $allow_docroot_outside_domains ? $user_home : $domains_base;
$params = read_params();
$action = trim((string)($params['action'] ?? ''));
$request_method = $_SERVER['REQUEST_METHOD'] ?? (getenv('REQUEST_METHOD') ?: 'GET');
$method = strtoupper((string)$request_method);
$csrf_token = get_csrf_token($plugin_data_dir, $da_user !== '' ? $da_user : 'unknown');
$csrf_actions = ['dir_list', 'dir_create', 'precheck_docroot', 'save_docroot', 'restore_default'];
$plugin_enabled = is_plugin_enabled_for_user($da_user, $custom_item_name, $custom_item_default);
if (in_array($action, $csrf_actions, true)) {
if ($method !== 'POST') {
ajax_output(['ok' => false, 'error' => t('Metoda niedozwolona.')], 405);
}
if (!csrf_is_valid($params, $csrf_token)) {
ajax_output(['ok' => false, 'error' => t('Nieprawidłowy token CSRF.')], 403);
}
if (!$plugin_enabled) {
ajax_output(['ok' => false, 'error' => t('Brak dostępu do pluginu. Włącz opcję docroot_changer w pakiecie lub dla użytkownika.')], 403);
}
}
$domains = list_user_domains($da_user);
$domains_lookup = array_fill_keys($domains, true);
$webserver = detect_webserver();
$targets = webserver_targets($webserver);
if ($action === 'dir_list') {
handle_dir_list($params, $picker_base);
}
if ($action === 'dir_create') {
handle_dir_create($params, $picker_base, $domains_base, $da_user, $allow_picker_create);
}
$domain_input = trim((string)($params['domain'] ?? ''));
$domain = normalize_domain_to_ascii($domain_input);
if ($action === 'precheck_docroot' || $action === 'save_docroot' || $action === 'restore_default') {
if (!is_valid_domain_name($domain)) {
ajax_output(['ok' => false, 'error' => t('Nieprawidłowa domena.')], 400);
}
if (!isset($domains_lookup[$domain])) {
ajax_output(['ok' => false, 'error' => t('Nie znaleziono domeny.')], 404);
}
$state = resolve_domain_docroot_state($da_user, $domain, $targets);
if ($action === 'precheck_docroot') {
$raw_docroot = (string)($params['docroot'] ?? '');
$validated = validate_docroot_path($raw_docroot, $domains_base, $user_home, $allow_docroot_outside_domains);
if (empty($validated['ok'])) {
ajax_output(['ok' => false, 'error' => (string)($validated['error'] ?? t('Docroot musi wskazywać istniejący katalog.'))], 400);
}
$new_docroot = (string)$validated['path'];
$owner_mismatch = false;
$owner = '';
$group = '';
if ($fix_ownership) {
$owner_check = path_owned_by_user($new_docroot, $da_user);
$owner_mismatch = empty($owner_check['match']);
$owner = (string)($owner_check['owner'] ?? '');
$group = (string)($owner_check['group'] ?? '');
}
ajax_output([
'ok' => true,
'path' => $new_docroot,
'owner_mismatch' => $owner_mismatch,
'owner' => $owner,
'group' => $group,
]);
}
if ($action === 'save_docroot') {
$raw_docroot = (string)($params['docroot'] ?? '');
$validated = validate_docroot_path($raw_docroot, $domains_base, $user_home, $allow_docroot_outside_domains);
if (empty($validated['ok'])) {
ajax_output(['ok' => false, 'error' => (string)($validated['error'] ?? t('Docroot musi wskazywać istniejący katalog.'))], 400);
}
$new_docroot = (string)$validated['path'];
$fix_owner = parse_bool((string)($params['fix_owner'] ?? '0'), false);
if ($fix_ownership) {
$owner_check = path_owned_by_user($new_docroot, $da_user);
if (empty($owner_check['match']) && !$fix_owner) {
ajax_output([
'ok' => false,
'error_code' => 'owner_mismatch',
'error' => t('Wskazany katalog nie należy do użytkownika docelowego.'),
'path' => $new_docroot,
'owner' => (string)($owner_check['owner'] ?? '?'),
'group' => (string)($owner_check['group'] ?? '?'),
], 409);
}
if (empty($owner_check['match']) && $fix_owner) {
if (!@chown($new_docroot, $da_user) || !@chgrp($new_docroot, $da_user)) {
ajax_output([
'ok' => false,
'error' => t('Nie udało się ustawić właściciela katalogu na użytkownika DirectAdmin.'),
], 500);
}
}
}
$applied = apply_docroot_configuration($da_user, $domain, $new_docroot, $targets);
if (empty($applied['ok'])) {
ajax_output(['ok' => false, 'error' => (string)($applied['error'] ?? t('Nie udało się zastosować zmiany docroot.'))], 500);
}
ajax_output([
'ok' => true,
'message' => t('Zapisano docroot dla domeny.'),
]);
}
if ($action === 'restore_default') {
$applied = apply_docroot_configuration($da_user, $domain, null, $targets);
if (empty($applied['ok'])) {
ajax_output(['ok' => false, 'error' => (string)($applied['error'] ?? t('Nie udało się zastosować zmiany docroot.'))], 500);
}
ajax_output([
'ok' => true,
'message' => t('Przywrócono domyślny docroot.'),
]);
}
}
$rows = [];
foreach ($domains as $d) {
$state = resolve_domain_docroot_state($da_user, $d, $targets);
$is_empty_docroot = directory_is_empty((string)$state['current_docroot']);
$rows[] = [
'domain' => $d,
'default_docroot' => (string)$state['default_docroot'],
'current_docroot' => (string)$state['current_docroot'],
'has_custom' => !empty($state['has_custom']),
'is_empty_docroot' => $is_empty_docroot,
'mismatch' => !empty($state['mismatch']),
'status_label' => !empty($state['has_custom']) ? t('Niestandardowy DocRoot') : t('Standardowy DocRoot'),
'status_class' => !empty($state['has_custom']) ? 'dc-status-custom' : 'dc-status-standard',
];
}
$domain_map = [];
foreach ($rows as $r) {
$domain_map[$r['domain']] = $r;
}
$is_cloudlinux_lsapi = (
$webserver === 'apache'
&& read_custombuild_option('php1_mode') === 'lsphp'
&& is_cloudlinux_system()
);
$webserver_label = $webserver;
if ($webserver === 'nginx_apache') {
$webserver_label = 'Nginx Reverse Proxy';
} elseif ($is_cloudlinux_lsapi) {
$webserver_label = 'CloudLinux LSAPI';
} elseif ($webserver === 'apache') {
$webserver_label = 'Apache';
} elseif ($webserver === 'nginx') {
$webserver_label = 'Nginx';
} elseif ($webserver === 'openlitespeed') {
$webserver_label = 'OpenLiteSpeed';
} elseif ($webserver === 'litespeed') {
$webserver_label = 'LiteSpeed Enterprise';
}
$skin = load_user_skin($da_user);
$css_file = $skin === 'evolution' ? 'evolution.css' : 'enhanced.css';
$inline_css = '';
$css_path = $plugin_dir . '/user/' . $css_file;
if (is_readable($css_path)) {
$inline_css = (string)file_get_contents($css_path);
} elseif (is_readable($plugin_dir . '/user/evolution.css')) {
$inline_css = (string)file_get_contents($plugin_dir . '/user/evolution.css');
}
?><!doctype html>
<html lang="<?php echo h($lang_code); ?>">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?php echo h(t('Zmiana katalogu domeny')); ?></title>
<?php if ($inline_css !== ''): ?>
<style><?php echo $inline_css; ?></style>
<?php endif; ?>
<style>
.dc-meta { margin-bottom: 12px; display:flex; gap:20px; flex-wrap:wrap; }
.dc-modal-field { margin-top: 10px; }
.dc-input-row { display:flex; gap:8px; align-items:center; }
.dc-input-row input[type="text"] { max-width: none; }
.dc-note { font-size: 13px; margin-top: 6px; }
.dc-browse-btn { min-width: 40px; padding-left: 10px; padding-right: 10px; }
.dc-folder-icon { width: 18px; height: 18px; display:inline-block; vertical-align: middle; }
.dc-folder-icon-inline { width: 16px; height: 16px; vertical-align: text-bottom; margin: 0 2px; }
.dc-actions { margin-top: 14px; display:flex; gap:8px; flex-wrap: wrap; justify-content:flex-end; }
.dc-hidden { display:none !important; }
.dc-picker-actions { display:flex; gap:8px; margin-top:10px; justify-content:flex-end; }
.dc-warning { color:#b45309; font-size:12px; }
.dc-warning-inline { color:#b45309; font-size:12px; font-weight:700; margin-left:8px; }
.dc-alert { margin-bottom: 10px; }
.dc-status-pill { display:inline-block; padding:4px 10px; border-radius:999px; font-size:12px; font-weight:700; }
.dc-status-standard { background:#dcfce7; color:#166534; }
.dc-status-custom { background:#fee2e2; color:#b91c1c; }
.dc-status-empty { color:#b45309; font-size:12px; font-weight:700; margin-top:6px; }
.dc-actions-col { display:flex; gap:8px; flex-wrap:wrap; }
.dc-actions-col .br-btn[disabled] { opacity:0.55; cursor:not-allowed; }
.dc-modal-warning { color:#b45309; font-size:12px; font-weight:700; margin-top:6px; }
.dc-picker-item { display:flex; justify-content:space-between; align-items:center; gap:8px; }
.dc-picker-empty-badge { color:#b45309; font-size:12px; font-weight:700; }
.dc-confirm-text { white-space: pre-line; line-height: 1.4; }
</style>
</head>
<body>
<div class="br-container">
<div class="br-header">
<div class="br-title"><?php echo h(t('Zmiana katalogu domeny')); ?></div>
</div>
<?php if (!$plugin_enabled): ?>
<div class="br-card">
<div class="br-alert br-alert-error"><?php echo h(t('Brak dostępu do pluginu. Włącz opcję docroot_changer w pakiecie lub dla użytkownika.')); ?></div>
</div>
<?php else: ?>
<div class="br-card">
<div class="dc-meta">
<div>
<strong><?php echo h(t('Wykryty webserwer:')); ?></strong> <?php echo h($webserver_label); ?>
<?php if ($webserver === 'nginx_apache'): ?>
<span class="dc-warning-inline"><?php echo h(t('Sprawdź ładowanie elementów statycznych, np. grafik lub stylów CSS po zmianie DocRoot Domeny')); ?></span>
<?php endif; ?>
</div>
</div>
</div>
<div class="br-card">
<div id="dc-alert" class="dc-alert br-alert br-alert-success dc-hidden"></div>
<table class="br-table">
<thead>
<tr>
<th><?php echo h(t('Domena')); ?></th>
<th><?php echo h(t('Aktualny Docroot')); ?></th>
<th><?php echo h(t('Obecne ustawienie')); ?></th>
<th><?php echo h(t('Akcje')); ?></th>
</tr>
</thead>
<tbody>
<?php if (empty($rows)): ?>
<tr><td colspan="4"><?php echo h(t('Brak domen do wyświetlenia.')); ?></td></tr>
<?php else: ?>
<?php foreach ($rows as $row): ?>
<tr>
<td><?php echo h($row['domain']); ?></td>
<td>
<code><?php echo h($row['current_docroot']); ?></code>
<?php if (!empty($row['mismatch'])): ?>
<div class="dc-warning"><?php echo h(t('Niezgodna konfiguracja Docroot między Apache i Nginx (pokazano pierwszą wykrytą).')); ?></div>
<?php endif; ?>
</td>
<td>
<span class="dc-status-pill <?php echo h($row['status_class']); ?>"><?php echo h($row['status_label']); ?></span>
<?php if (!empty($row['is_empty_docroot'])): ?>
<div class="dc-status-empty"><?php echo h(t('Wskazany katalog jest pusty')); ?></div>
<?php endif; ?>
</td>
<td>
<div class="dc-actions-col">
<button class="br-btn br-btn-ghost" type="button" onclick="openDomainModal('<?php echo h($row['domain']); ?>')"><?php echo h(t('Zmień katalog domeny')); ?></button>
<button class="br-btn br-btn-ghost" type="button" onclick="restoreDefaultForDomain('<?php echo h($row['domain']); ?>')" <?php echo !empty($row['has_custom']) ? '' : 'disabled'; ?>><?php echo h(t('Ustaw domyślną')); ?></button>
</div>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
<div id="dc-domain-modal" class="br-overlay dc-hidden">
<div class="br-modal">
<div class="br-modal-header">
<div class="br-modal-title" id="dc-modal-title"><?php echo h(t('Zmień katalog domeny')); ?></div>
<button class="br-icon-btn" type="button" onclick="closeDomainModal()" aria-label="<?php echo h(t('Zamknij')); ?>"></button>
</div>
<div class="br-modal-body">
<div class="dc-modal-field">
<strong id="dc-domain-name"></strong>
</div>
<div class="dc-modal-field">
<label for="dc-docroot-input"><?php echo h(t('Docroot dla domeny:')); ?></label>
<div class="dc-input-row">
<input id="dc-docroot-input" type="text" autocomplete="off">
<button class="br-btn br-btn-ghost dc-browse-btn" type="button" onclick="openPicker('dc-docroot-input')" aria-label="<?php echo h(t('Przeglądaj')); ?>" title="<?php echo h(t('Przeglądaj')); ?>">
<img class="dc-folder-icon" src="/CMD_PLUGINS/docroot-changer/images/folder.svg" alt="<?php echo h(t('Przeglądaj')); ?>" title="<?php echo h(t('Przeglądaj')); ?>">
</button>
</div>
<div class="dc-note br-muted" id="dc-docroot-help"></div>
<div class="dc-modal-warning dc-hidden" id="dc-modal-warning"></div>
</div>
<div id="dc-modal-error" class="br-alert br-alert-error dc-hidden"></div>
</div>
<div class="dc-actions">
<button class="br-btn br-btn-ghost" type="button" onclick="closeDomainModal()"><?php echo h(t('Anuluj')); ?></button>
<button class="br-btn" type="button" onclick="saveDocroot()"><?php echo h(t('Zapisz')); ?></button>
</div>
</div>
</div>
<div id="dc-picker" class="br-overlay dc-hidden">
<div class="br-modal">
<div class="br-modal-header">
<div class="br-modal-title"><?php echo h(t('Wybierz katalog')); ?></div>
<button class="br-icon-btn" type="button" onclick="closePicker()" aria-label="<?php echo h(t('Zamknij')); ?>"></button>
</div>
<div class="br-modal-body">
<div class="br-picker-path">
<button class="br-btn br-btn-ghost br-btn-small" type="button" onclick="pickerGoUp()"><?php echo h(t('W górę')); ?></button>
<span id="dc-picker-current" class="br-muted"></span>
</div>
<div id="dc-picker-error" class="br-alert br-alert-error dc-hidden"></div>
<ul id="dc-picker-list" class="br-picker-list"></ul>
<?php if ($allow_picker_create): ?>
<div class="br-picker-create" id="dc-picker-create-wrap">
<input id="dc-picker-new" type="text" placeholder="<?php echo h(t('Nowy katalog')); ?>">
<button class="br-btn br-btn-ghost" type="button" onclick="pickerCreateDir()"><?php echo h(t('Utwórz')); ?></button>
</div>
<?php endif; ?>
</div>
<div class="dc-picker-actions">
<button class="br-btn br-btn-ghost" type="button" onclick="closePicker()"><?php echo h(t('Anuluj')); ?></button>
<button class="br-btn" type="button" onclick="selectPickerPath()"><?php echo h(t('Wybierz')); ?></button>
</div>
</div>
</div>
<div id="dc-confirm-modal" class="br-overlay dc-hidden">
<div class="br-modal">
<div class="br-modal-header">
<div class="br-modal-title"><?php echo h(t('Potwierdzenie')); ?></div>
<button class="br-icon-btn" type="button" onclick="closeConfirmModal(false)" aria-label="<?php echo h(t('Zamknij')); ?>"></button>
</div>
<div class="br-modal-body">
<div id="dc-confirm-text" class="dc-confirm-text"></div>
</div>
<div class="dc-actions">
<button class="br-btn br-btn-ghost" type="button" onclick="closeConfirmModal(false)"><?php echo h(t('Nie')); ?></button>
<button class="br-btn" type="button" onclick="closeConfirmModal(true)"><?php echo h(t('Tak')); ?></button>
</div>
</div>
</div>
<script>
(function () {
var PLUGIN_URL = '/CMD_PLUGINS/docroot-changer';
var CSRF_TOKEN = <?php echo json_encode($csrf_token); ?>;
var FLASH_SUCCESS_KEY = 'docroot_changer_success_message';
var DA_USER = <?php echo json_encode($da_user); ?>;
var BASE_DOMAINS = <?php echo json_encode($domains_base); ?>;
var USER_HOME = <?php echo json_encode($user_home); ?>;
var ALLOW_DOCROOT_OUTSIDE_DOMAINS = <?php echo $allow_docroot_outside_domains ? 'true' : 'false'; ?>;
var PICKER_CREATE_ENABLED = <?php echo $allow_picker_create ? 'true' : 'false'; ?>;
var DISABLE_CONFIRMATIONS = <?php echo $disable_confirmations ? 'true' : 'false'; ?>;
var DOMAIN_DATA = <?php echo json_encode($domain_map, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); ?>;
var I18N = {
method_not_allowed: <?php echo json_encode(t('Metoda niedozwolona.')); ?>,
csrf_error: <?php echo json_encode(t('Nieprawidłowy token CSRF.')); ?>,
request_failed: <?php echo json_encode(t('Nie udało się wysłać żądania.')); ?>,
parse_failed: <?php echo json_encode(t('Nie udało się przetworzyć odpowiedzi.')); ?>,
path_prefix: <?php echo json_encode(t('Ścieżka musi zaczynać się od:')); ?>,
path_inside: <?php echo json_encode(t('Ścieżka musi znajdować się w katalogu:')); ?>,
path_dotdot: <?php echo json_encode(t('Ścieżka zawiera niedozwolony fragment "..".')); ?>,
no_dirs: <?php echo json_encode(t('Brak katalogów')); ?>,
picker_load_error: <?php echo json_encode(t('Nie udało się wczytać katalogów.')); ?>,
picker_create_error: <?php echo json_encode(t('Nie udało się utworzyć katalogu.')); ?>,
picker_name_required: <?php echo json_encode(t('Podaj nazwę katalogu.')); ?>,
picker_exists: <?php echo json_encode(t('Katalog już istnieje.')); ?>,
picker_invalid_name: <?php echo json_encode(t('Nieprawidłowa nazwa katalogu.')); ?>,
picker_main_domains: <?php echo json_encode(t('Nie można utworzyć katalogu bezpośrednio w ')); ?>,
picker_create_disabled: <?php echo json_encode(t('Tworzenie katalogów jest wyłączone.')); ?>,
picker_outside: <?php echo json_encode(t('Nie można utworzyć katalogu poza dozwoloną ścieżką.')); ?>,
path_inside_home: <?php echo json_encode(t('Ścieżka musi znajdować się w katalogu użytkownika:')); ?>,
path_not_starts_with: <?php echo json_encode(t('Ścieżka nie zaczyna się od:')); ?>,
empty_dir_warning: <?php echo json_encode(t('Wskazany katalog jest pusty')); ?>,
empty_dir_short: <?php echo json_encode(t('pusty')); ?>,
change_custom_confirm: <?php echo json_encode(t('Czy na pewno chcesz zmienić katalog domeny __DOMAIN__ na niestandardowy?')); ?>,
restore_default_confirm: <?php echo json_encode(t('Czy na pewno chcesz przywrócić domyślny katalog domeny __DOMAIN__')); ?>,
current_docroot_label: <?php echo json_encode(t('Obecny katalog domeny:')); ?>,
new_docroot_label: <?php echo json_encode(t('Nowy katalog domeny:')); ?>,
default_docroot_label: <?php echo json_encode(t('Domyślny katalog domeny:')); ?>,
owner_mismatch_confirm_template: <?php echo json_encode(t('Wskazany katalog ma właściciela __OWNER__:__GROUP__. Czy kontynuować? W przypadku kontynuacji wskazany katalog __PATH__ będzie miał zmienione uprawnienia na __USER__:__USER__. Zmiana dotyczy wyłącznie tego katalogu (bez podkatalogów).')); ?>,
success_change_template: <?php echo json_encode(t('Zmiana katalogu domeny __DOMAIN__ wykonana pomyślnie będzie ona widoczna w ciągu kilku minut')); ?>,
browse_label: <?php echo json_encode(t('Przeglądaj')); ?>,
modal_help_template: <?php echo json_encode(t('Aby zmienić DocRoot dla domeny __DOMAIN__ Wpisz ścieżkę do katalogu docelowego, lub użyj przycisku __ICON__ aby wskazać go w drzewie katalogów.')); ?>
};
var activeDomain = '';
var activeInput = null;
var pickerPath = '';
var pickerBase = '';
var pickerCurrentEmpty = false;
var selectedPathEmpty = false;
var confirmResolver = null;
function normalizePath(value) {
var v = (value || '').trim();
if (!v) { return ''; }
v = v.replace(/\\/g, '/').replace(/\/+/, '/');
if (v.charAt(0) !== '/') {
v = '/' + v;
}
v = v.replace(/\/+/g, '/');
if (v.length > 1 && v.endsWith('/')) {
v = v.replace(/\/+$/, '');
}
return v;
}
function isInsideBase(path, base) {
if (!path || !base) { return false; }
if (path === base) { return true; }
return (path + '/').indexOf(base + '/') === 0;
}
function showAlert(message, type) {
var box = document.getElementById('dc-alert');
if (!box) { return; }
box.className = 'dc-alert br-alert';
box.classList.add(type === 'error' ? 'br-alert-error' : 'br-alert-success');
box.textContent = message || '';
box.classList.remove('dc-hidden');
}
function persistSuccessAndReload(message) {
try {
if (window.sessionStorage) {
window.sessionStorage.setItem(FLASH_SUCCESS_KEY, message || '');
}
} catch (e) {}
window.location.reload();
}
function showFlashAlertIfExists() {
try {
if (!window.sessionStorage) { return; }
var msg = window.sessionStorage.getItem(FLASH_SUCCESS_KEY) || '';
if (!msg) { return; }
window.sessionStorage.removeItem(FLASH_SUCCESS_KEY);
showAlert(msg, 'success');
} catch (e) {}
}
function showModalError(message) {
var box = document.getElementById('dc-modal-error');
if (!box) { return; }
if (!message) {
box.classList.add('dc-hidden');
box.innerHTML = '';
return;
}
var html = escapeHtml(message || '').replace(/__ICON__/g, folderIconHtml());
box.innerHTML = html;
box.classList.remove('dc-hidden');
}
function showModalWarning(messages) {
var box = document.getElementById('dc-modal-warning');
if (!box) { return; }
if (!messages || !messages.length) {
box.innerHTML = '';
box.classList.add('dc-hidden');
return;
}
box.innerHTML = messages.map(escapeHtml).join('<br>');
box.classList.remove('dc-hidden');
}
function postAction(action, extra) {
var marker = '__DOCROOT_AJAX_JSON__';
var params = new URLSearchParams();
params.append('action', action);
params.append('csrf_token', CSRF_TOKEN || '');
if (extra) {
Object.keys(extra).forEach(function (key) {
params.append(key, extra[key]);
});
}
function parsePayload(text) {
var start = text.indexOf(marker);
if (start !== -1) {
var end = text.indexOf(marker, start + marker.length);
if (end !== -1) {
var chunk = text.substring(start + marker.length, end);
return JSON.parse(chunk);
}
}
return JSON.parse(text);
}
return fetch(PLUGIN_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
},
body: params.toString(),
credentials: 'same-origin'
}).then(function (res) {
return res.text().then(function (text) {
var data;
try {
data = parsePayload(text);
} catch (e) {
throw new Error(I18N.parse_failed || 'Parse error');
}
if (!res.ok || !data || data.ok !== true) {
var msg = (data && data.error) ? data.error : ('HTTP ' + res.status);
var err = new Error(msg);
if (data && data.error_code) {
err.code = data.error_code;
}
if (data) {
err.data = data;
}
throw err;
}
return data;
});
});
}
function openConfirmModal(message) {
return new Promise(function (resolve) {
var modal = document.getElementById('dc-confirm-modal');
var text = document.getElementById('dc-confirm-text');
if (text) { text.textContent = message || ''; }
confirmResolver = resolve;
if (modal) { modal.classList.remove('dc-hidden'); }
});
}
function askForConfirmation(message) {
if (DISABLE_CONFIRMATIONS) {
return Promise.resolve(true);
}
return openConfirmModal(message);
}
function closeConfirmModal(result) {
var modal = document.getElementById('dc-confirm-modal');
if (modal) { modal.classList.add('dc-hidden'); }
if (confirmResolver) {
var resolver = confirmResolver;
confirmResolver = null;
resolver(!!result);
}
}
window.closeConfirmModal = closeConfirmModal;
function closeDomainModal() {
var modal = document.getElementById('dc-domain-modal');
if (modal) { modal.classList.add('dc-hidden'); }
activeDomain = '';
showModalError('');
showModalWarning([]);
}
window.closeDomainModal = closeDomainModal;
function withDomain(template, domain) {
return String(template || '').replace(/__DOMAIN__/g, domain || '');
}
function escapeHtml(value) {
return String(value || '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
function folderIconHtml() {
var alt = escapeHtml(I18N.browse_label || 'Przeglądaj');
return '<img class="dc-folder-icon dc-folder-icon-inline" src="' + PLUGIN_URL + '/images/folder.svg" alt="' + alt + '" title="' + alt + '">';
}
function buildSuccessMessage(domain) {
return withDomain(I18N.success_change_template, domain);
}
function buildCustomConfirmMessage(domain, currentPath, newPath) {
return [
withDomain(I18N.change_custom_confirm, domain),
(I18N.current_docroot_label || 'Obecny katalog domeny:') + ' ' + (currentPath || '-'),
(I18N.new_docroot_label || 'Nowy katalog domeny:') + ' ' + (newPath || '-')
].join('\n');
}
function buildDefaultConfirmMessage(domain, currentPath, defaultPath) {
return [
withDomain(I18N.restore_default_confirm, domain),
(I18N.current_docroot_label || 'Obecny katalog domeny:') + ' ' + (currentPath || '-'),
(I18N.default_docroot_label || 'Domyślny katalog domeny:') + ' ' + (defaultPath || '-')
].join('\n');
}
function buildOwnerMismatchConfirmMessage(path, owner, group) {
return String(I18N.owner_mismatch_confirm_template || '')
.replace(/__PATH__/g, path || '')
.replace(/__OWNER__/g, owner || '?')
.replace(/__GROUP__/g, group || '?')
.replace(/__USER__/g, DA_USER || '');
}
function refreshDocrootWarnings(path, emptySelected) {
var warnings = [];
var domainsBase = normalizePath(BASE_DOMAINS);
var userHome = normalizePath(USER_HOME);
if (path && domainsBase && !isInsideBase(path, domainsBase)) {
warnings.push((I18N.path_not_starts_with || 'Ścieżka nie zaczyna się od:') + ' ' + domainsBase);
}
if (path && userHome && !isInsideBase(path, userHome)) {
warnings.push((I18N.path_inside_home || 'Ścieżka musi znajdować się w katalogu użytkownika:') + ' ' + userHome);
}
if (emptySelected === true) {
warnings.push(I18N.empty_dir_warning || 'Wskazany katalog jest pusty');
}
showModalWarning(warnings);
}
window.openDomainModal = function (domain) {
var data = DOMAIN_DATA[domain] || null;
if (!data) { return; }
activeDomain = domain;
var modal = document.getElementById('dc-domain-modal');
var title = document.getElementById('dc-modal-title');
var name = document.getElementById('dc-domain-name');
var input = document.getElementById('dc-docroot-input');
var help = document.getElementById('dc-docroot-help');
if (title) { title.textContent = (<?php echo json_encode(t('Zmień katalog domeny')); ?> + ' - ' + domain); }
if (name) { name.textContent = domain; }
if (input) { input.value = data.current_docroot || ''; }
selectedPathEmpty = !!data.is_empty_docroot;
if (help) {
var helpText = withDomain(I18N.modal_help_template, '<strong>' + escapeHtml(domain) + '</strong>');
help.innerHTML = helpText.replace(/__ICON__/g, folderIconHtml());
}
showModalError('');
refreshDocrootWarnings(normalizePath(data.current_docroot || ''), selectedPathEmpty);
if (modal) { modal.classList.remove('dc-hidden'); }
};
window.saveDocroot = function () {
if (!activeDomain) { return; }
var domain = activeDomain;
var input = document.getElementById('dc-docroot-input');
if (!input) { return; }
var path = normalizePath(input.value || '');
var home = normalizePath(USER_HOME);
if (!path || path.indexOf('..') !== -1) {
showModalError(I18N.path_dotdot || 'Invalid path');
return;
}
if (!path.startsWith('/')) {
showModalError((I18N.path_prefix || 'Path must start with:') + ' /');
return;
}
if (!isInsideBase(path, home)) {
showModalError((I18N.path_inside_home || 'Ścieżka musi znajdować się w katalogu użytkownika:') + ' ' + home);
return;
}
refreshDocrootWarnings(path, selectedPathEmpty);
var currentData = DOMAIN_DATA[domain] || {};
var currentPath = currentData.current_docroot || '';
askForConfirmation(buildCustomConfirmMessage(domain, currentPath, path))
.then(function (confirmed) {
if (!confirmed) { return null; }
return postAction('save_docroot', { domain: domain, docroot: path });
})
.then(function (data) {
if (!data) { return; }
closeDomainModal();
persistSuccessAndReload(buildSuccessMessage(domain));
})
.catch(function (err) {
if (err && err.code === 'owner_mismatch') {
var info = err.data || {};
var ownerWarn = buildOwnerMismatchConfirmMessage(
String(info.path || path),
String(info.owner || '?'),
String(info.group || '?')
);
askForConfirmation(ownerWarn)
.then(function (confirmedOwnerFix) {
if (!confirmedOwnerFix) { return null; }
return postAction('save_docroot', { domain: domain, docroot: path, fix_owner: '1' });
})
.then(function (retryData) {
if (!retryData) { return; }
closeDomainModal();
persistSuccessAndReload(buildSuccessMessage(domain));
})
.catch(function (retryErr) {
showModalError(retryErr.message || (I18N.request_failed || 'Request failed'));
});
return;
}
showModalError(err.message || (I18N.request_failed || 'Request failed'));
});
};
window.restoreDefaultForDomain = function (domain) {
var data = DOMAIN_DATA[domain] || null;
if (!data) { return; }
askForConfirmation(buildDefaultConfirmMessage(domain, data.current_docroot || '', data.default_docroot || ''))
.then(function (confirmed) {
if (!confirmed) { return null; }
return postAction('restore_default', { domain: domain });
})
.then(function (data) {
if (!data) { return; }
closeDomainModal();
persistSuccessAndReload(buildSuccessMessage(domain));
})
.catch(function (err) {
showAlert(err.message || (I18N.request_failed || 'Request failed'), 'error');
});
};
function pickerSetError(message) {
var box = document.getElementById('dc-picker-error');
if (!box) { return; }
if (!message) {
box.classList.add('dc-hidden');
box.textContent = '';
return;
}
box.textContent = message;
box.classList.remove('dc-hidden');
}
function renderPickerEntries(entries) {
var list = document.getElementById('dc-picker-list');
if (!list) { return; }
list.innerHTML = '';
if (!entries || !entries.length) {
var li = document.createElement('li');
li.textContent = I18N.no_dirs || 'No directories';
list.appendChild(li);
return;
}
entries.forEach(function (entry) {
var li = document.createElement('li');
var wrap = document.createElement('div');
wrap.className = 'dc-picker-item';
var name = document.createElement('span');
name.textContent = entry.name || '';
wrap.appendChild(name);
if (entry && entry.empty === true) {
var badge = document.createElement('span');
badge.className = 'dc-picker-empty-badge';
badge.textContent = I18N.empty_dir_short || 'pusty';
wrap.appendChild(badge);
}
li.appendChild(wrap);
li.addEventListener('click', function () {
var full = (pickerPath.replace(/\/+$/, '') + '/' + (entry.name || '')).replace(/\/+/g, '/');
loadPicker(full);
});
list.appendChild(li);
});
}
function applyPickerSelection(path, isEmpty) {
if (!activeInput) {
closePicker();
return;
}
if (path) {
activeInput.value = path;
}
selectedPathEmpty = !!isEmpty;
refreshDocrootWarnings(normalizePath(activeInput.value || ''), selectedPathEmpty);
closePicker();
}
function loadPicker(path) {
pickerSetError('');
postAction('dir_list', { path: path || '' })
.then(function (data) {
pickerPath = data.path || '';
pickerBase = data.base || '';
pickerCurrentEmpty = !!data.current_empty;
var current = document.getElementById('dc-picker-current');
if (current) { current.textContent = pickerPath; }
updatePickerCreateVisibility();
renderPickerEntries(data.entries || []);
})
.catch(function (err) {
pickerSetError(err.message || (I18N.picker_load_error || 'Load error'));
});
}
function updatePickerCreateVisibility() {
var wrap = document.getElementById('dc-picker-create-wrap');
if (!wrap) { return; }
var domainsBase = normalizePath(BASE_DOMAINS);
var current = normalizePath(pickerPath || '');
if (!PICKER_CREATE_ENABLED || (domainsBase && current === domainsBase)) {
wrap.classList.add('dc-hidden');
} else {
wrap.classList.remove('dc-hidden');
}
}
window.pickerCreateDir = function () {
if (!PICKER_CREATE_ENABLED) {
pickerSetError(I18N.picker_create_disabled || 'Tworzenie katalogów jest wyłączone.');
return;
}
var input = document.getElementById('dc-picker-new');
if (!input) { return; }
var name = (input.value || '').trim();
if (!name) {
pickerSetError(I18N.picker_name_required || 'Directory name required');
return;
}
postAction('dir_create', { path: pickerPath || '', name: name })
.then(function (data) {
input.value = '';
loadPicker(data.created || pickerPath || '');
})
.catch(function (err) {
var msg = err.message || '';
if (msg === 'exists') { msg = I18N.picker_exists || msg; }
else if (msg === 'invalid_name') { msg = I18N.picker_invalid_name || msg; }
else if (msg === 'outside_base') { msg = I18N.picker_outside || msg; }
else if (msg === 'main_domain_blocked') { msg = (I18N.picker_main_domains || 'Nie można utworzyć katalogu bezpośrednio w ') + normalizePath(BASE_DOMAINS) + '.'; }
else if (msg === 'name_required') { msg = I18N.picker_name_required || msg; }
else if (msg === 'mkdir_failed' || msg === 'chmod_failed' || msg === 'chown_failed' || msg === 'invalid_user') { msg = I18N.picker_create_error || msg; }
else if (msg === 'create_disabled') { msg = I18N.picker_create_disabled || msg; }
pickerSetError(msg || (I18N.picker_create_error || 'Create error'));
});
};
window.openPicker = function (inputId) {
activeInput = document.getElementById(inputId);
if (!activeInput) { return; }
var modal = document.getElementById('dc-picker');
if (modal) { modal.classList.remove('dc-hidden'); }
var start = normalizePath(BASE_DOMAINS);
loadPicker(start);
};
window.closePicker = function () {
var modal = document.getElementById('dc-picker');
if (modal) { modal.classList.add('dc-hidden'); }
pickerSetError('');
};
window.pickerGoUp = function () {
if (!pickerPath || !pickerBase || pickerPath === pickerBase) { return; }
var up = pickerPath.replace(/\/+$/, '');
var idx = up.lastIndexOf('/');
if (idx <= 0) {
up = pickerBase;
} else {
up = up.substring(0, idx);
}
if (!up) { up = pickerBase; }
loadPicker(up);
};
window.selectPickerPath = function () {
applyPickerSelection(pickerPath || '', !!pickerCurrentEmpty);
};
var docrootInput = document.getElementById('dc-docroot-input');
if (docrootInput) {
docrootInput.addEventListener('input', function () {
selectedPathEmpty = false;
refreshDocrootWarnings(normalizePath(docrootInput.value || ''), false);
});
}
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape') {
closeConfirmModal(false);
closePicker();
closeDomainModal();
}
});
showFlashAlertIfExists();
})();
</script>
<?php endif; ?>
</div>
</body>
</html>