2070 lines
74 KiB
HTML
Executable File
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, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
}
|
|
|
|
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>
|