#!/usr/local/bin/php -nc/usr/local/directadmin/plugins/docroot-changer/php.ini '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'); } ?> <?php echo h(t('Zmiana katalogu domeny')); ?>