commit 4c883fd543c1c38ed6627964d72cf2ef0f3962fd Author: Marek Miklewicz Date: Sat Jul 4 12:02:31 2026 +0200 Initial import of docroot-changer diff --git a/hooks/menu_user.sh b/hooks/menu_user.sh new file mode 100755 index 0000000..d1c85e5 --- /dev/null +++ b/hooks/menu_user.sh @@ -0,0 +1,40 @@ +#!/bin/sh + +user="${USERNAME:-${LOGNAME:-${USER:-}}}" +lang="${LANGUAGE:-}" + +normalize_lang() { + val="$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]')" + case "$val" in + pl*|polish*) echo "pl" ;; + en*|english*) echo "en" ;; + *) echo "" ;; + esac +} + +is_enabled_for_user() { + u="$1" + [ -n "$u" ] || return 1 + cfg="/usr/local/directadmin/data/users/$u/user.conf" + [ -r "$cfg" ] || return 1 + val="$(grep -E '^docroot_changer=' "$cfg" 2>/dev/null | tail -n1 | cut -d= -f2-)" + case "$(printf '%s' "$val" | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]')" in + yes|on|true|1) return 0 ;; + esac + return 1 +} + +lang="$(normalize_lang "$lang")" +if [ -z "$lang" ] && [ -n "$user" ] && [ -r "/usr/local/directadmin/data/users/$user/user.conf" ]; then + cfg_lang="$(grep -i -m1 -E '^(lang|language|locale)=' "/usr/local/directadmin/data/users/$user/user.conf" | cut -d= -f2-)" + lang="$(normalize_lang "$cfg_lang")" +fi + +label="Zmiana katalogu domeny" + +if ! is_enabled_for_user "$user"; then + printf '[]\n' + exit 0 +fi + +printf '[{"name":"%s","icon":"","entries":[{"name":"%s","icon":"","href":"/CMD_PLUGINS/docroot-changer","newTab":false,"updates":0}]}]\n' "$label" "$label" diff --git a/hooks/user_txt.html b/hooks/user_txt.html new file mode 100644 index 0000000..665f2ae --- /dev/null +++ b/hooks/user_txt.html @@ -0,0 +1 @@ +Zmiana katalogu domeny diff --git a/images/folder.svg b/images/folder.svg new file mode 100644 index 0000000..fa9e88a --- /dev/null +++ b/images/folder.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/images/user_icon.svg b/images/user_icon.svg new file mode 100644 index 0000000..c3df5dc --- /dev/null +++ b/images/user_icon.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + / + + + + + + + + diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..5a6fbdb --- /dev/null +++ b/install.sh @@ -0,0 +1,11 @@ +#!/bin/sh +set -eu + +PLUGIN_DIR="$(cd "$(dirname "$0")" && pwd)" +SCRIPT="$PLUGIN_DIR/scripts/install.sh" + +if [ ! -x "$SCRIPT" ]; then + chmod 755 "$SCRIPT" 2>/dev/null || true +fi + +exec "$SCRIPT" diff --git a/lang/en.php b/lang/en.php new file mode 100644 index 0000000..16f800b --- /dev/null +++ b/lang/en.php @@ -0,0 +1,99 @@ + 'Docroot Changer', + 'Zmiana katalogu domeny' => 'Change domain directory', + 'Docroot Changer' => 'Docroot Changer', + 'Domena' => 'Domain', + 'Aktualny Docroot' => 'Current Docroot', + 'Tryb' => 'Mode', + 'Domyślny' => 'Default', + 'Niestandardowy' => 'Custom', + 'Akcje' => 'Actions', + 'Edytuj' => 'Edit', + 'Zmień katalog domeny' => 'Change domain directory', + 'Ustaw domyślną' => 'Set default', + 'Brak domen do wyświetlenia.' => 'No domains to display.', + 'Wykryty webserwer:' => 'Detected web server:', + 'Sprawdź ładowanie elementów statycznych, np. grafik lub stylów CSS po zmianie DocRoot Domeny' => 'Check loading of static assets (for example images or CSS) after changing the domain DocRoot', + 'Dozwolona baza ścieżek:' => 'Allowed path base:', + 'Zapisz' => 'Save', + 'Anuluj' => 'Cancel', + 'Potwierdzenie' => 'Confirmation', + 'Przywróć domyślną' => 'Restore default', + 'Przywróć poprzedni Docroot' => 'Restore previous Docroot', + 'Obecne ustawienie' => 'Current setting', + 'Standardowy DocRoot' => 'Standard DocRoot', + 'Niestandardowy DocRoot' => 'Custom DocRoot', + 'Przeglądaj' => 'Browse', + 'Wybierz katalog' => 'Select directory', + 'Wybierz' => 'Select', + 'W górę' => 'Up', + 'Nowy katalog' => 'New directory', + 'Utwórz' => 'Create', + 'Brak katalogów' => 'No directories', + 'Nie udało się wczytać katalogów.' => 'Failed to load directories.', + 'Nie udało się utworzyć katalogu.' => 'Failed to create directory.', + 'Nieprawidłowa nazwa katalogu.' => 'Invalid directory name.', + 'Katalog już istnieje.' => 'Directory already exists.', + 'Nie można utworzyć katalogu poza dozwoloną ścieżką.' => 'Cannot create directory outside the allowed path.', + 'Nie można utworzyć katalogu bezpośrednio w ' => 'Cannot create a directory directly in ', + 'Tworzenie katalogów jest wyłączone.' => 'Directory creation is disabled.', + 'Podaj nazwę katalogu.' => 'Provide a directory name.', + 'Ścieżka musi zaczynać się od:' => 'Path must start with:', + 'Ścieżka nie zaczyna się od:' => 'Path does not start with:', + 'ścieżka zawiera nieprawidłowe znaki - proszę wprowadzić poprawną ścieżkę lub skorzystać z opcji __ICON__ w celu wyboru poprawnie ścieżki.' => 'Path contains invalid characters - please enter a valid path or use the __ICON__ option to choose a valid path.', + 'Ścieżka zawiera niedozwolony fragment "..".' => 'Path contains forbidden ".." segment.', + 'Docroot musi wskazywać istniejący katalog.' => 'Docroot must point to an existing directory.', + 'Docroot musi być katalogiem.' => 'Docroot must be a directory.', + 'Brak dostępu do katalogu docroot.' => 'No access to the docroot directory.', + 'Nieprawidłowy token CSRF.' => 'Invalid CSRF token.', + 'Metoda niedozwolona.' => 'Method Not Allowed.', + 'Brak dostępu do pluginu. Włącz opcję docroot_changer w pakiecie lub dla użytkownika.' => 'No access to the plugin. Enable docroot_changer in package or for the user.', + 'Brak dostępu' => 'Access denied', + 'Nieprawidłowa domena.' => 'Invalid domain.', + 'Nie znaleziono domeny.' => 'Domain not found.', + 'Wskazany katalog nie należy do użytkownika docelowego.' => 'The selected directory does not belong to the target user.', + 'Nie udało się ustawić właściciela katalogu na użytkownika DirectAdmin.' => 'Failed to set directory owner to the DirectAdmin user.', + 'Nie udało się zapisać poprzedniego docroot.' => 'Failed to save previous docroot.', + 'Nie znaleziono poprzedniego docroot dla domeny.' => 'No previous docroot found for this domain.', + 'Zapisano docroot dla domeny.' => 'Docroot saved for domain.', + 'Przywrócono domyślny docroot.' => 'Default docroot restored.', + 'Przywrócono poprzedni docroot.' => 'Previous docroot restored.', + 'Błąd API DirectAdmin:' => 'DirectAdmin API error:', + 'Brak odpowiedzi z API DirectAdmin.' => 'No response from DirectAdmin API.', + 'Brak aktywnej sesji DirectAdmin do wywołania API.' => 'No active DirectAdmin session for API call.', + 'Nieobsługiwany webserwer, używam trybu apache.' => 'Unsupported web server, using apache mode.', + 'Kliknij nazwę domeny, aby zmienić Docroot.' => 'Click a domain name to change Docroot.', + 'Docroot dla domeny:' => 'Docroot for domain:', + 'Wpisz ścieżkę ręcznie lub użyj pickera katalogów.' => 'Enter a path manually or use the directory picker.', + 'Aby zmienić DocRoot dla domeny __DOMAIN__ Wpisz ścieżkę do katalogu docelowego, lub użyj przycisku __ICON__ aby wskazać go w drzewie katalogów.' => 'To change DocRoot for domain __DOMAIN__, enter the target directory path, or use the __ICON__ button to select it in the directory tree.', + 'Czy na pewno chcesz zmienić katalog domeny __DOMAIN__ na niestandardowy?' => 'Are you sure you want to set a custom domain directory for __DOMAIN__?', + 'Czy na pewno chcesz przywrócić domyślny katalog domeny __DOMAIN__' => 'Are you sure you want to restore the default domain directory for __DOMAIN__?', + 'Obecny katalog domeny:' => 'Current domain directory:', + 'Nowy katalog domeny:' => 'New domain directory:', + 'Domyślny katalog domeny:' => 'Default domain directory:', + '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).' => 'The selected directory owner is __OWNER__:__GROUP__. Continue? If you continue, __PATH__ will be changed to __USER__:__USER__. This change applies only to this directory (without subdirectories).', + 'Zmiana katalogu domeny __DOMAIN__ wykonana pomyślnie będzie ona widoczna w ciągu kilku minut' => 'Domain directory change for __DOMAIN__ completed successfully; it will be visible within a few minutes', + 'Zamknij' => 'Close', + 'Kod błędu:' => 'Error code:', + 'Nie udało się wysłać żądania.' => 'Failed to send request.', + 'Nie udało się przetworzyć odpowiedzi.' => 'Failed to process response.', + 'Przywrócić domyślny docroot dla tej domeny?' => 'Restore default docroot for this domain?', + 'Przywrócić poprzedni docroot dla tej domeny?' => 'Restore previous docroot for this domain?', + 'Niezgodna konfiguracja Docroot między Apache i Nginx (pokazano pierwszą wykrytą).' => 'Docroot mismatch between Apache and Nginx (showing first detected).', + 'Tak' => 'Yes', + 'Nie' => 'No', + 'Ścieżka musi znajdować się w katalogu:' => 'Path must be inside:', + 'Ścieżka musi znajdować się w katalogu użytkownika:' => 'Path must be inside user directory:', + 'Nie udało się usunąć pliku konfiguracji webserwera: ' => 'Failed to remove web server configuration file: ', + 'Nie udało się odczytać pliku konfiguracji webserwera: ' => 'Failed to read web server configuration file: ', + 'Nie udało się zapisać pliku konfiguracji webserwera: ' => 'Failed to write web server configuration file: ', + 'Nie udało się zainicjować przebudowy konfiguracji przez dataskq.' => 'Failed to queue configuration rebuild via dataskq.', + 'Nieprawidłowa nazwa użytkownika dla zadania rewrite httpd.' => 'Invalid username for httpd rewrite task.', + 'Nie udało się dodać zadania rewrite httpd do task.queue.' => 'Failed to append httpd rewrite task to task.queue.', + 'Nie udało się zainicjować przebudowy konfiguracji httpd przez task.queue/dataskq.' => 'Failed to trigger httpd configuration rebuild via task.queue/dataskq.', + 'Nie udało się zastosować zmiany docroot.' => 'Failed to apply docroot change.', + 'wprowadzona ścieżka jest dowiązaniem symbolicznym do: __TARGET__, wprowadź ścieżkę do realnego katalogu' => 'The entered path is a symbolic link to: __TARGET__, enter a path to the real directory', + 'Wskazany katalog jest pusty' => 'The selected directory is empty', + 'pusty' => 'empty', +]; diff --git a/lang/pl.php b/lang/pl.php new file mode 100644 index 0000000..478b5ba --- /dev/null +++ b/lang/pl.php @@ -0,0 +1,7 @@ + $_value) { + $pl[$key] = $key; +} +return $pl; diff --git a/php.ini b/php.ini new file mode 100644 index 0000000..9cb2498 --- /dev/null +++ b/php.ini @@ -0,0 +1,7 @@ +[PHP] +short_open_tag=On +variables_order=GPCS +error_reporting=E_ALL & ~E_NOTICE +display_errors=Off +log_errors=On +output_buffering=4096 diff --git a/plugin-settings.conf b/plugin-settings.conf new file mode 100644 index 0000000..22fe62c --- /dev/null +++ b/plugin-settings.conf @@ -0,0 +1,17 @@ +# Ustawienia pluginu docroot-changer +# CUSTOM_PACKAGE_ITEM_NAME - nazwa custom package item w DirectAdmin. +CUSTOM_PACKAGE_ITEM_NAME=docroot_changer +# CUSTOM_PACKAGE_ITEM_DEFAULT - domyślna wartość custom package item. +CUSTOM_PACKAGE_ITEM_DEFAULT=no +# ENFORCE_DOMAINS_BASE - ścieżka bazowa dozwolona dla docroot. +ENFORCE_DOMAINS_BASE=/home/DA_USER/domains +# ALLOW_PICKER_CREATE_DIR - pozwala tworzyć katalogi z poziomu pickera (yes/no). +ALLOW_PICKER_CREATE_DIR=yes +# ALLOW_DOCROOT_OUTSIDE_DOMAINS_DIR - pozwala ustawić DocRoot poza /home/DA_USER/domains. +# Gdy yes, dozwolony zakres to tylko /home/DA_USER (nigdy powyżej katalogu użytkownika). +ALLOW_DOCROOT_OUTSIDE_DOMAINS_DIR=no +# DISABLE_CONFIRMATIONS - wyłącza popupy potwierdzeń (true/false). +DISABLE_CONFIRMATIONS=false +# FIX_OWNERSHIP - sprawdza owner katalogu i w razie potrzeby pozwala poprawić owner na DA_USER:DA_USER. +# Ustaw false, aby pominąć sprawdzanie ownera, ostrzeżenia i zmianę ownera. +FIX_OWNERSHIP=true diff --git a/plugin.conf b/plugin.conf new file mode 100644 index 0000000..ded99a0 --- /dev/null +++ b/plugin.conf @@ -0,0 +1,6 @@ +name=Docroot-Changer +id=docroot-changer +type=user +author=HITME.PL +version=2.1.2 +user_run_as=root diff --git a/scripts/install.sh b/scripts/install.sh new file mode 100755 index 0000000..e205c48 --- /dev/null +++ b/scripts/install.sh @@ -0,0 +1,38 @@ +#!/bin/bash +set -euo pipefail + +PLUGIN_DIR="$(cd "$(dirname "$0")/.." && pwd)" + +if [ "$(id -u)" -eq 0 ]; then + chown -R diradmin:diradmin "$PLUGIN_DIR" || true +else + echo "docroot-changer: warning: not running as root, cannot chown to diradmin" >&2 +fi + +chmod 755 "$PLUGIN_DIR" "$PLUGIN_DIR/hooks" "$PLUGIN_DIR/user" "$PLUGIN_DIR/scripts" "$PLUGIN_DIR/images" || true +chmod 755 "$PLUGIN_DIR/user/index.html" "$PLUGIN_DIR/install.sh" "$PLUGIN_DIR/uninstall.sh" "$PLUGIN_DIR/scripts/install.sh" "$PLUGIN_DIR/scripts/uninstall.sh" "$PLUGIN_DIR/hooks/menu_user.sh" || true +chmod 644 "$PLUGIN_DIR/plugin.conf" "$PLUGIN_DIR/plugin-settings.conf" "$PLUGIN_DIR/hooks/user_txt.html" "$PLUGIN_DIR/user/evolution.css" "$PLUGIN_DIR/images/user_icon.svg" "$PLUGIN_DIR/images/folder.svg" || true + +DATA_DIR="$PLUGIN_DIR/data" +mkdir -p "$DATA_DIR/csrf" +chmod 700 "$DATA_DIR" "$DATA_DIR/csrf" || true +if [ "$(id -u)" -eq 0 ]; then + chown -R diradmin:diradmin "$DATA_DIR" || true +fi + +CPI_FILE="/usr/local/directadmin/data/admin/custom_package_items.conf" +ITEM_LINE='docroot_changer=checkbox=no&desc=Allow+Docroot+Changer+plugin' +if [ "$(id -u)" -eq 0 ]; then + mkdir -p "$(dirname "$CPI_FILE")" + touch "$CPI_FILE" + if grep -q '^docroot_changer=' "$CPI_FILE"; then + sed -i.bak '/^docroot_changer=/d' "$CPI_FILE" + fi + printf '%s\n' "$ITEM_LINE" >> "$CPI_FILE" + chmod 644 "$CPI_FILE" || true + chown diradmin:diradmin "$CPI_FILE" || true +else + echo "docroot-changer: warning: not running as root, cannot modify custom_package_items.conf" >&2 +fi + +echo "docroot-changer: install complete" diff --git a/scripts/uninstall.sh b/scripts/uninstall.sh new file mode 100755 index 0000000..6119bed --- /dev/null +++ b/scripts/uninstall.sh @@ -0,0 +1,22 @@ +#!/bin/bash +set -euo pipefail + +PLUGIN_DIR="$(cd "$(dirname "$0")/.." && pwd)" + +if [ "$(basename "$PLUGIN_DIR")" != "docroot-changer" ]; then + echo "docroot-changer: safety check failed (basename is not docroot-changer): $PLUGIN_DIR" >&2 + exit 1 +fi +if [ ! -f "$PLUGIN_DIR/plugin.conf" ]; then + echo "docroot-changer: safety check failed (plugin.conf missing): $PLUGIN_DIR" >&2 + exit 1 +fi + +CPI_FILE="/usr/local/directadmin/data/admin/custom_package_items.conf" +if [ "$(id -u)" -eq 0 ] && [ -f "$CPI_FILE" ]; then + sed -i.bak '/^docroot_changer=/d' "$CPI_FILE" || true +fi + +(sleep 3; rm -rf "$PLUGIN_DIR") >/dev/null 2>&1 & + +echo "docroot-changer: uninstall complete" diff --git a/uninstall.sh b/uninstall.sh new file mode 100755 index 0000000..4cc4c95 --- /dev/null +++ b/uninstall.sh @@ -0,0 +1,11 @@ +#!/bin/sh +set -eu + +PLUGIN_DIR="$(cd "$(dirname "$0")" && pwd)" +SCRIPT="$PLUGIN_DIR/scripts/uninstall.sh" + +if [ ! -x "$SCRIPT" ]; then + chmod 755 "$SCRIPT" 2>/dev/null || true +fi + +exec "$SCRIPT" diff --git a/user/enhanced.css b/user/enhanced.css new file mode 100644 index 0000000..da26a53 --- /dev/null +++ b/user/enhanced.css @@ -0,0 +1,347 @@ +:root { + --br-bg: #f5f8ff; + --br-card: #ffffff; + --br-text: #111827; + --br-muted: #64748b; + --br-border: #e2e8f0; + --br-accent: #2563eb; + --br-accent-dark: #1d4ed8; + --br-danger: #dc2626; + --br-warn: #f59e0b; + --br-shadow: 0 10px 28px rgba(15, 23, 42, 0.12); +} +* { box-sizing: border-box; } +body { + margin: 0; + background: var(--br-bg); + color: var(--br-text); + font-family: "IBM Plex Sans", "Segoe UI", Arial, sans-serif; +} +.br-container { + max-width: none; + width: 100%; + margin: 16px auto; + padding: 16px 24px; +} +.br-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 16px; +} +.br-title { + font-size: 20px; + font-weight: 700; + letter-spacing: 0.2px; +} +.br-card { + background: var(--br-card); + border: 1px solid var(--br-border); + border-radius: 12px; + padding: 16px; + box-shadow: var(--br-shadow); + margin-bottom: 16px; +} +.br-muted { color: var(--br-muted); } +.br-alert { + border-radius: 10px; + padding: 10px 12px; + margin: 8px 0; + border: 1px solid transparent; +} +.br-alert-error { background: #fff1f2; border-color: #fecdd3; color: #9f1239; } +.br-alert-warn { background: #fffbeb; border-color: #fde68a; color: #92400e; } +.br-alert-success { background: #ecfdf3; border-color: #bbf7d0; color: #166534; } +.br-overlay { + position: fixed; + inset: 0; + background: rgba(15, 23, 42, 0.35); + display: flex; + align-items: center; + justify-content: center; + z-index: 9999; +} +.br-modal { + width: min(560px, 92vw); + background: #ffffff; + border-radius: 12px; + padding: 16px; + border: 2px solid var(--br-border); + box-shadow: var(--br-shadow); + display: flex; + flex-direction: column; + max-height: 90vh; +} +.br-modal-log { + width: 90vw; + background: #ffffff; +} +.br-modal-warning { + background: #f3c8c8; +} +.br-modal-warning .br-btn-ghost { + background: #ffffff; +} +.br-modal-warning .br-btn-ghost:hover { + background: #f8fafc; +} +.br-modal-danger { + border-color: var(--br-danger); +} +.br-modal-actions { + display: flex; + justify-content: flex-end; + gap: 8px; + margin-top: 12px; +} +.br-modal-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + margin-bottom: 10px; +} +.br-modal-title { + font-weight: 700; + font-size: 16px; +} +.br-modal-body { + max-height: 60vh; + overflow: auto; +} +.br-modal-scroll { + overflow: auto; + flex: 1; + min-height: 0; +} +.br-log-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} +.br-log-header-actions { + display: flex; + gap: 8px; +} +.br-log-meta { + display: grid; + gap: 6px; + margin: 10px 0 12px; + padding: 10px 12px; + border: 1px solid var(--br-border); + border-radius: 10px; + background: #f8fafc; + font-size: 13px; +} +.br-log-actions { + margin: 8px 0 10px; +} +.br-picker-path { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 10px; + flex-wrap: wrap; +} +.br-picker-list { + list-style: none; + padding: 0; + margin: 0; + border: 1px solid var(--br-border); + border-radius: 10px; + max-height: 45vh; + overflow: auto; +} +.br-picker-create { + margin-top: 10px; + display: flex; + gap: 8px; +} +.br-picker-create input { + flex: 1; + padding: 6px 8px; + border: 1px solid var(--br-border); + border-radius: 8px; +} +.br-picker-list li { + padding: 8px 12px; + cursor: pointer; + border-bottom: 1px solid var(--br-border); +} +.br-picker-list li:last-child { + border-bottom: none; +} +.br-picker-list li:hover { + background: #f8fafc; +} +.br-picker-list li.active { + background: #d1fae5; +} +.br-btn-small { + padding: 6px 10px; + font-size: 12px; +} +.br-actions-row { + display: flex; + gap: 8px; + margin-top: 10px; + flex-wrap: wrap; +} +.br-actions-row form { + margin: 0; +} +.br-icon-btn { + border: 1px solid var(--br-border); + background: #fff; + border-radius: 8px; + padding: 4px 8px; + cursor: pointer; + font-size: 16px; + line-height: 1; +} +.br-icon-btn:hover { + background: #f8fafc; +} +.br-table { + width: 100%; + border-collapse: collapse; + border: 1px solid var(--br-border); + border-radius: 10px; + overflow: hidden; + background: #fff; + table-layout: fixed; +} +.br-table th { + text-align: left; + font-size: 12px; + text-transform: uppercase; + letter-spacing: 0.04em; + color: var(--br-muted); + background: #f1f5f9; + padding: 10px; +} +.br-table td { + padding: 10px; + border-top: 1px solid var(--br-border); + vertical-align: top; +} +.br-table tr:nth-child(even) td { background: #f8fafc; } +.br-col-select { width: 50px; text-align: center; } +.br-col-status { width: 120px; } +.br-col-progress { width: 90px; } +.br-col-mode { width: 180px; } +.br-col-user { width: 140px; } +.br-col-backup { width: 220px; overflow-wrap: anywhere; } +.br-col-path { width: 260px; overflow-wrap: anywhere; } +.br-col-target { width: 220px; overflow-wrap: anywhere; } +.br-col-time { width: 150px; } +.br-col-actions { width: 120px; } +.br-table input[type="checkbox"] { + accent-color: var(--br-accent); + width: 16px; + height: 16px; +} +.br-actions { display: flex; gap: 8px; align-items: center; margin: 10px 0; } +.br-btn { + background: var(--br-accent); + color: #fff; + border: none; + border-radius: 8px; + padding: 8px 14px; + cursor: pointer; + font-weight: 600; +} +.br-btn:hover { background: var(--br-accent-dark); } +.br-btn-ghost { + background: transparent; + color: var(--br-text); + border: 1px solid var(--br-border); +} +.br-btn-ghost:hover { background: #e2e8f0; } +.br-btn-danger { + background: var(--br-danger); +} +.br-btn-danger:hover { background: #b91c1c; } +.br-link-danger { color: var(--br-danger); text-decoration: none; } +.br-link-danger:hover { text-decoration: underline; } +.br-inline-form { display: inline; } +.br-link-button { + background: none; + border: none; + padding: 0; + margin: 0; + color: inherit; + cursor: pointer; + font: inherit; +} +.br-link-button:hover { text-decoration: underline; } +.br-link-button.br-link-danger { color: var(--br-danger); } +input[type="text"] { + width: 100%; + max-width: 680px; + padding: 8px 10px; + border: 1px solid var(--br-border); + border-radius: 8px; + font-size: 14px; + background: #fff; +} +pre { + background: #0b1020; + color: #e2e8f0; + padding: 12px; + border-radius: 8px; + overflow: auto; +} +a { color: var(--br-accent-dark); text-decoration: none; } +a:hover { text-decoration: underline; } +.br-toast { + position: fixed; + bottom: 18px; + right: 18px; + background: #16a34a; + color: #fff; + padding: 12px 16px; + border-radius: 10px; + box-shadow: 0 12px 28px rgba(16, 24, 40, 0.25); + z-index: 9999; + font-weight: 600; +} +.br-toast-stack { + position: fixed; + top: 18px; + right: 18px; + display: flex; + flex-direction: column; + gap: 8px; + z-index: 9999; +} +.br-toast-stack .br-toast { + position: static; + top: auto; + bottom: auto; +} +.br-toast-error { + background: #dc2626; + top: 18px; + bottom: auto; +} +.br-toast-success { + background: #16a34a; +} +.br-status-line { + margin: 6px 0; + font-size: 14px; +} +.br-progress { + height: 8px; + background: #e5e7eb; + border-radius: 999px; + overflow: hidden; + margin: 6px 0 12px; +} +.br-progress-bar { + height: 100%; + background: var(--br-accent); + transition: width 0.3s ease; +} diff --git a/user/evolution.css b/user/evolution.css new file mode 100644 index 0000000..da26a53 --- /dev/null +++ b/user/evolution.css @@ -0,0 +1,347 @@ +:root { + --br-bg: #f5f8ff; + --br-card: #ffffff; + --br-text: #111827; + --br-muted: #64748b; + --br-border: #e2e8f0; + --br-accent: #2563eb; + --br-accent-dark: #1d4ed8; + --br-danger: #dc2626; + --br-warn: #f59e0b; + --br-shadow: 0 10px 28px rgba(15, 23, 42, 0.12); +} +* { box-sizing: border-box; } +body { + margin: 0; + background: var(--br-bg); + color: var(--br-text); + font-family: "IBM Plex Sans", "Segoe UI", Arial, sans-serif; +} +.br-container { + max-width: none; + width: 100%; + margin: 16px auto; + padding: 16px 24px; +} +.br-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 16px; +} +.br-title { + font-size: 20px; + font-weight: 700; + letter-spacing: 0.2px; +} +.br-card { + background: var(--br-card); + border: 1px solid var(--br-border); + border-radius: 12px; + padding: 16px; + box-shadow: var(--br-shadow); + margin-bottom: 16px; +} +.br-muted { color: var(--br-muted); } +.br-alert { + border-radius: 10px; + padding: 10px 12px; + margin: 8px 0; + border: 1px solid transparent; +} +.br-alert-error { background: #fff1f2; border-color: #fecdd3; color: #9f1239; } +.br-alert-warn { background: #fffbeb; border-color: #fde68a; color: #92400e; } +.br-alert-success { background: #ecfdf3; border-color: #bbf7d0; color: #166534; } +.br-overlay { + position: fixed; + inset: 0; + background: rgba(15, 23, 42, 0.35); + display: flex; + align-items: center; + justify-content: center; + z-index: 9999; +} +.br-modal { + width: min(560px, 92vw); + background: #ffffff; + border-radius: 12px; + padding: 16px; + border: 2px solid var(--br-border); + box-shadow: var(--br-shadow); + display: flex; + flex-direction: column; + max-height: 90vh; +} +.br-modal-log { + width: 90vw; + background: #ffffff; +} +.br-modal-warning { + background: #f3c8c8; +} +.br-modal-warning .br-btn-ghost { + background: #ffffff; +} +.br-modal-warning .br-btn-ghost:hover { + background: #f8fafc; +} +.br-modal-danger { + border-color: var(--br-danger); +} +.br-modal-actions { + display: flex; + justify-content: flex-end; + gap: 8px; + margin-top: 12px; +} +.br-modal-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + margin-bottom: 10px; +} +.br-modal-title { + font-weight: 700; + font-size: 16px; +} +.br-modal-body { + max-height: 60vh; + overflow: auto; +} +.br-modal-scroll { + overflow: auto; + flex: 1; + min-height: 0; +} +.br-log-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} +.br-log-header-actions { + display: flex; + gap: 8px; +} +.br-log-meta { + display: grid; + gap: 6px; + margin: 10px 0 12px; + padding: 10px 12px; + border: 1px solid var(--br-border); + border-radius: 10px; + background: #f8fafc; + font-size: 13px; +} +.br-log-actions { + margin: 8px 0 10px; +} +.br-picker-path { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 10px; + flex-wrap: wrap; +} +.br-picker-list { + list-style: none; + padding: 0; + margin: 0; + border: 1px solid var(--br-border); + border-radius: 10px; + max-height: 45vh; + overflow: auto; +} +.br-picker-create { + margin-top: 10px; + display: flex; + gap: 8px; +} +.br-picker-create input { + flex: 1; + padding: 6px 8px; + border: 1px solid var(--br-border); + border-radius: 8px; +} +.br-picker-list li { + padding: 8px 12px; + cursor: pointer; + border-bottom: 1px solid var(--br-border); +} +.br-picker-list li:last-child { + border-bottom: none; +} +.br-picker-list li:hover { + background: #f8fafc; +} +.br-picker-list li.active { + background: #d1fae5; +} +.br-btn-small { + padding: 6px 10px; + font-size: 12px; +} +.br-actions-row { + display: flex; + gap: 8px; + margin-top: 10px; + flex-wrap: wrap; +} +.br-actions-row form { + margin: 0; +} +.br-icon-btn { + border: 1px solid var(--br-border); + background: #fff; + border-radius: 8px; + padding: 4px 8px; + cursor: pointer; + font-size: 16px; + line-height: 1; +} +.br-icon-btn:hover { + background: #f8fafc; +} +.br-table { + width: 100%; + border-collapse: collapse; + border: 1px solid var(--br-border); + border-radius: 10px; + overflow: hidden; + background: #fff; + table-layout: fixed; +} +.br-table th { + text-align: left; + font-size: 12px; + text-transform: uppercase; + letter-spacing: 0.04em; + color: var(--br-muted); + background: #f1f5f9; + padding: 10px; +} +.br-table td { + padding: 10px; + border-top: 1px solid var(--br-border); + vertical-align: top; +} +.br-table tr:nth-child(even) td { background: #f8fafc; } +.br-col-select { width: 50px; text-align: center; } +.br-col-status { width: 120px; } +.br-col-progress { width: 90px; } +.br-col-mode { width: 180px; } +.br-col-user { width: 140px; } +.br-col-backup { width: 220px; overflow-wrap: anywhere; } +.br-col-path { width: 260px; overflow-wrap: anywhere; } +.br-col-target { width: 220px; overflow-wrap: anywhere; } +.br-col-time { width: 150px; } +.br-col-actions { width: 120px; } +.br-table input[type="checkbox"] { + accent-color: var(--br-accent); + width: 16px; + height: 16px; +} +.br-actions { display: flex; gap: 8px; align-items: center; margin: 10px 0; } +.br-btn { + background: var(--br-accent); + color: #fff; + border: none; + border-radius: 8px; + padding: 8px 14px; + cursor: pointer; + font-weight: 600; +} +.br-btn:hover { background: var(--br-accent-dark); } +.br-btn-ghost { + background: transparent; + color: var(--br-text); + border: 1px solid var(--br-border); +} +.br-btn-ghost:hover { background: #e2e8f0; } +.br-btn-danger { + background: var(--br-danger); +} +.br-btn-danger:hover { background: #b91c1c; } +.br-link-danger { color: var(--br-danger); text-decoration: none; } +.br-link-danger:hover { text-decoration: underline; } +.br-inline-form { display: inline; } +.br-link-button { + background: none; + border: none; + padding: 0; + margin: 0; + color: inherit; + cursor: pointer; + font: inherit; +} +.br-link-button:hover { text-decoration: underline; } +.br-link-button.br-link-danger { color: var(--br-danger); } +input[type="text"] { + width: 100%; + max-width: 680px; + padding: 8px 10px; + border: 1px solid var(--br-border); + border-radius: 8px; + font-size: 14px; + background: #fff; +} +pre { + background: #0b1020; + color: #e2e8f0; + padding: 12px; + border-radius: 8px; + overflow: auto; +} +a { color: var(--br-accent-dark); text-decoration: none; } +a:hover { text-decoration: underline; } +.br-toast { + position: fixed; + bottom: 18px; + right: 18px; + background: #16a34a; + color: #fff; + padding: 12px 16px; + border-radius: 10px; + box-shadow: 0 12px 28px rgba(16, 24, 40, 0.25); + z-index: 9999; + font-weight: 600; +} +.br-toast-stack { + position: fixed; + top: 18px; + right: 18px; + display: flex; + flex-direction: column; + gap: 8px; + z-index: 9999; +} +.br-toast-stack .br-toast { + position: static; + top: auto; + bottom: auto; +} +.br-toast-error { + background: #dc2626; + top: 18px; + bottom: auto; +} +.br-toast-success { + background: #16a34a; +} +.br-status-line { + margin: 6px 0; + font-size: 14px; +} +.br-progress { + height: 8px; + background: #e5e7eb; + border-radius: 999px; + overflow: hidden; + margin: 6px 0 12px; +} +.br-progress-bar { + height: 100%; + background: var(--br-accent); + transition: width 0.3s ease; +} diff --git a/user/index.html b/user/index.html new file mode 100755 index 0000000..7bdaddd --- /dev/null +++ b/user/index.html @@ -0,0 +1,2069 @@ +#!/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')); ?> + + + + + + +
+
+
+
+ + +
+
+
+ +
+
+
+ + + + +
+
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+ + +
+ +
+
+ + +
+
+
+ +
+
+
+
+ +
+
+
+ +
+
+ +
+ + +
+
+
+
+
+
+
+ + +
+
+
+ +
+
+
+
+ +
+
+
+ + +
+
+
    + +
    + + +
    + +
    +
    + + +
    +
    +
    + +
    +
    +
    +
    + +
    +
    +
    +
    +
    + + +
    +
    +
    + + + +
    + +