2025-10-08 14:23:07 +03:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace App\Controllers;
|
|
|
|
|
|
|
|
|
|
use Core\Controller;
|
|
|
|
|
use App\Models\Domain;
|
|
|
|
|
use App\Models\NotificationGroup;
|
|
|
|
|
use App\Services\WhoisService;
|
|
|
|
|
|
|
|
|
|
class DomainController extends Controller
|
|
|
|
|
{
|
|
|
|
|
private Domain $domainModel;
|
|
|
|
|
private NotificationGroup $groupModel;
|
|
|
|
|
private WhoisService $whoisService;
|
|
|
|
|
|
|
|
|
|
public function __construct()
|
|
|
|
|
{
|
|
|
|
|
$this->domainModel = new Domain();
|
|
|
|
|
$this->groupModel = new NotificationGroup();
|
|
|
|
|
$this->whoisService = new WhoisService();
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-20 21:08:09 +03:00
|
|
|
/**
|
|
|
|
|
* Check domain access based on isolation mode
|
|
|
|
|
*/
|
|
|
|
|
private function checkDomainAccess(int $id): ?array
|
|
|
|
|
{
|
|
|
|
|
$userId = \Core\Auth::id();
|
|
|
|
|
$settingModel = new \App\Models\Setting();
|
|
|
|
|
$isolationMode = $settingModel->getValue('user_isolation_mode', 'shared');
|
|
|
|
|
|
|
|
|
|
if ($isolationMode === 'isolated') {
|
|
|
|
|
return $this->domainModel->findWithIsolation($id, $userId);
|
|
|
|
|
} else {
|
|
|
|
|
return $this->domainModel->find($id);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-08 14:23:07 +03:00
|
|
|
public function index()
|
|
|
|
|
{
|
2025-10-20 17:04:13 +03:00
|
|
|
// Get current user and isolation mode
|
|
|
|
|
$userId = \Core\Auth::id();
|
|
|
|
|
$settingModel = new \App\Models\Setting();
|
|
|
|
|
$isolationMode = $settingModel->getValue('user_isolation_mode', 'shared');
|
|
|
|
|
|
2025-10-08 14:23:07 +03:00
|
|
|
// Get filter parameters
|
Add CSRF, CAPTCHA, and input validation improvements
Introduces CSRF protection to all sensitive controller actions, integrates configurable CAPTCHA (reCAPTCHA v2/v3, Turnstile) for authentication and registration flows, and centralizes input validation via a new InputValidator helper. Adds new helpers and services for CSRF and CAPTCHA, updates settings and migration for CAPTCHA configuration, and enhances logging and error handling in TLD registry import processes. Also improves validation for user, domain, group, and profile inputs throughout the application.
2025-10-10 00:04:12 +03:00
|
|
|
$search = \App\Helpers\InputValidator::sanitizeSearch($_GET['search'] ?? '', 100);
|
2025-10-08 14:23:07 +03:00
|
|
|
$status = $_GET['status'] ?? '';
|
|
|
|
|
$groupId = $_GET['group'] ?? '';
|
2025-10-12 12:46:16 +03:00
|
|
|
$tag = $_GET['tag'] ?? '';
|
2025-10-08 14:23:07 +03:00
|
|
|
$sortBy = $_GET['sort'] ?? 'domain_name';
|
|
|
|
|
$sortOrder = $_GET['order'] ?? 'asc';
|
|
|
|
|
$page = max(1, (int)($_GET['page'] ?? 1));
|
|
|
|
|
$perPage = max(10, min(100, (int)($_GET['per_page'] ?? 25))); // Between 10 and 100
|
|
|
|
|
|
2025-10-08 18:54:34 +03:00
|
|
|
// Get expiring threshold from settings
|
|
|
|
|
$notificationDays = $settingModel->getNotificationDays();
|
|
|
|
|
$expiringThreshold = !empty($notificationDays) ? max($notificationDays) : 30;
|
|
|
|
|
|
2025-10-10 14:01:19 +03:00
|
|
|
// Prepare filters array
|
|
|
|
|
$filters = [
|
|
|
|
|
'search' => $search,
|
|
|
|
|
'status' => $status,
|
2025-10-12 12:46:16 +03:00
|
|
|
'group' => $groupId,
|
|
|
|
|
'tag' => $tag
|
2025-10-10 14:01:19 +03:00
|
|
|
];
|
2025-10-08 14:23:07 +03:00
|
|
|
|
2025-10-10 14:01:19 +03:00
|
|
|
// Get filtered and paginated domains using model
|
2025-10-20 18:13:57 +03:00
|
|
|
$result = $this->domainModel->getFilteredPaginated($filters, $sortBy, $sortOrder, $page, $perPage, $expiringThreshold, $isolationMode === 'isolated' ? $userId : null);
|
2025-10-08 14:23:07 +03:00
|
|
|
|
2025-10-20 18:13:57 +03:00
|
|
|
// Get groups and tags based on isolation mode
|
|
|
|
|
if ($isolationMode === 'isolated') {
|
|
|
|
|
$groups = $this->groupModel->getAllWithChannelCount($userId);
|
|
|
|
|
$allTags = $this->domainModel->getAllTags($userId);
|
|
|
|
|
} else {
|
|
|
|
|
$groups = $this->groupModel->getAllWithChannelCount();
|
|
|
|
|
$allTags = $this->domainModel->getAllTags();
|
|
|
|
|
}
|
2025-10-12 12:46:16 +03:00
|
|
|
|
2025-10-25 02:04:00 +03:00
|
|
|
// Get available tags for bulk operations
|
|
|
|
|
$tagModel = new \App\Models\Tag();
|
|
|
|
|
if ($isolationMode === 'isolated') {
|
|
|
|
|
$availableTags = $tagModel->getAllWithUsage($userId);
|
|
|
|
|
} else {
|
|
|
|
|
$availableTags = $tagModel->getAllWithUsage();
|
|
|
|
|
}
|
|
|
|
|
|
Upgraded to 1.1.0
1.1.0 (2025-10-09)
- **User Notifications System** - In-app notification center with 7 notification types, filtering, pagination
- **Advanced Session Management** - Database-backed sessions with geolocation (country, city, ISP)
- **Remote Session Control** - Terminate any device instantly with immediate logout validation
- **Enhanced Profile Page** - Sidebar navigation with 4 tabs, hash-based routing (#profile, #security, #sessions)
- **MVC Architecture Refactoring** - 3 new Helpers (Layout, Domain, Session), ~265 lines cleaned from views
- **Geolocation Tracking** - IP-based location detection using ip-api.com, country flags with flag-icons
- **Device Detection** - Browser & device type parsing (Chrome/Firefox/Safari, Desktop/Mobile/Tablet)
- **Auto-Detected Cron Paths** - Settings show actual installation paths (thanks @jadeops)
- **Welcome Notifications** - Sent to new users on registration or fresh install
- **Upgrade Notifications** - Admins notified on system updates with version & migration count
- **Web-Based Installer** - Replaces CLI, auto-generates encryption key, one-time password display
- **Web-Based Updater** - `/install/update` for running new migrations with smart detection
- **User Registration** - Full signup flow with email verification, password reset, resend verification
- **User Management** - CRUD for users with filtering, sorting, pagination (admin-only)
- **Remember Me** - 30-day secure tokens linked to sessions, cascade deletion on logout
- **Session Validator** - Middleware validates sessions on every request for instant remote logout
- **Consistent UI/UX** - Unified filtering, sorting, pagination across Domains, Users, Notifications, TLD Registry
- **Smart Migrations** - Consolidated schema for fresh installs, incremental for upgrades
- **XSS Protection** - htmlspecialchars() applied across all user-facing data (thanks @jadeops)
2025-10-09 18:02:46 +03:00
|
|
|
// Format domains for display
|
2025-10-10 14:01:19 +03:00
|
|
|
$formattedDomains = \App\Helpers\DomainHelper::formatMultiple($result['domains']);
|
2025-10-08 14:23:07 +03:00
|
|
|
|
2025-10-20 17:40:43 +03:00
|
|
|
// Get users for transfer functionality (admin only)
|
|
|
|
|
$users = [];
|
|
|
|
|
if (\Core\Auth::isAdmin()) {
|
|
|
|
|
$userModel = new \App\Models\User();
|
|
|
|
|
$users = $userModel->all();
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-08 14:23:07 +03:00
|
|
|
$this->view('domains/index', [
|
Upgraded to 1.1.0
1.1.0 (2025-10-09)
- **User Notifications System** - In-app notification center with 7 notification types, filtering, pagination
- **Advanced Session Management** - Database-backed sessions with geolocation (country, city, ISP)
- **Remote Session Control** - Terminate any device instantly with immediate logout validation
- **Enhanced Profile Page** - Sidebar navigation with 4 tabs, hash-based routing (#profile, #security, #sessions)
- **MVC Architecture Refactoring** - 3 new Helpers (Layout, Domain, Session), ~265 lines cleaned from views
- **Geolocation Tracking** - IP-based location detection using ip-api.com, country flags with flag-icons
- **Device Detection** - Browser & device type parsing (Chrome/Firefox/Safari, Desktop/Mobile/Tablet)
- **Auto-Detected Cron Paths** - Settings show actual installation paths (thanks @jadeops)
- **Welcome Notifications** - Sent to new users on registration or fresh install
- **Upgrade Notifications** - Admins notified on system updates with version & migration count
- **Web-Based Installer** - Replaces CLI, auto-generates encryption key, one-time password display
- **Web-Based Updater** - `/install/update` for running new migrations with smart detection
- **User Registration** - Full signup flow with email verification, password reset, resend verification
- **User Management** - CRUD for users with filtering, sorting, pagination (admin-only)
- **Remember Me** - 30-day secure tokens linked to sessions, cascade deletion on logout
- **Session Validator** - Middleware validates sessions on every request for instant remote logout
- **Consistent UI/UX** - Unified filtering, sorting, pagination across Domains, Users, Notifications, TLD Registry
- **Smart Migrations** - Consolidated schema for fresh installs, incremental for upgrades
- **XSS Protection** - htmlspecialchars() applied across all user-facing data (thanks @jadeops)
2025-10-09 18:02:46 +03:00
|
|
|
'domains' => $formattedDomains,
|
2025-10-08 14:23:07 +03:00
|
|
|
'groups' => $groups,
|
2025-10-12 12:46:16 +03:00
|
|
|
'allTags' => $allTags,
|
2025-10-25 02:04:00 +03:00
|
|
|
'availableTags' => $availableTags,
|
2025-10-20 17:40:43 +03:00
|
|
|
'users' => $users,
|
2025-10-08 14:23:07 +03:00
|
|
|
'filters' => [
|
|
|
|
|
'search' => $search,
|
|
|
|
|
'status' => $status,
|
|
|
|
|
'group' => $groupId,
|
2025-10-12 12:46:16 +03:00
|
|
|
'tag' => $tag,
|
2025-10-08 14:23:07 +03:00
|
|
|
'sort' => $sortBy,
|
|
|
|
|
'order' => $sortOrder
|
|
|
|
|
],
|
2025-10-10 14:01:19 +03:00
|
|
|
'pagination' => $result['pagination'],
|
2025-10-08 14:23:07 +03:00
|
|
|
'title' => 'Domains'
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function create()
|
|
|
|
|
{
|
2025-10-20 18:50:32 +03:00
|
|
|
// Get groups based on isolation mode
|
|
|
|
|
$userId = \Core\Auth::id();
|
|
|
|
|
$settingModel = new \App\Models\Setting();
|
|
|
|
|
$isolationMode = $settingModel->getValue('user_isolation_mode', 'shared');
|
|
|
|
|
|
|
|
|
|
if ($isolationMode === 'isolated') {
|
|
|
|
|
$groups = $this->groupModel->getAllWithChannelCount($userId);
|
|
|
|
|
} else {
|
|
|
|
|
$groups = $this->groupModel->getAllWithChannelCount();
|
|
|
|
|
}
|
2025-10-25 02:04:00 +03:00
|
|
|
|
|
|
|
|
// Get available tags for the new tag system
|
|
|
|
|
$tagModel = new \App\Models\Tag();
|
|
|
|
|
if ($isolationMode === 'isolated') {
|
|
|
|
|
$availableTags = $tagModel->getAllWithUsage($userId);
|
|
|
|
|
} else {
|
|
|
|
|
$availableTags = $tagModel->getAllWithUsage();
|
|
|
|
|
}
|
2025-10-08 14:23:07 +03:00
|
|
|
|
|
|
|
|
$this->view('domains/create', [
|
|
|
|
|
'groups' => $groups,
|
2025-10-25 02:04:00 +03:00
|
|
|
'availableTags' => $availableTags,
|
2025-10-08 14:23:07 +03:00
|
|
|
'title' => 'Add Domain'
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function store()
|
|
|
|
|
{
|
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
|
|
|
|
$this->redirect('/domains/create');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
Add CSRF, CAPTCHA, and input validation improvements
Introduces CSRF protection to all sensitive controller actions, integrates configurable CAPTCHA (reCAPTCHA v2/v3, Turnstile) for authentication and registration flows, and centralizes input validation via a new InputValidator helper. Adds new helpers and services for CSRF and CAPTCHA, updates settings and migration for CAPTCHA configuration, and enhances logging and error handling in TLD registry import processes. Also improves validation for user, domain, group, and profile inputs throughout the application.
2025-10-10 00:04:12 +03:00
|
|
|
// CSRF Protection
|
|
|
|
|
$this->verifyCsrf('/domains/create');
|
|
|
|
|
|
2025-10-08 14:23:07 +03:00
|
|
|
$domainName = trim($_POST['domain_name'] ?? '');
|
|
|
|
|
$groupId = !empty($_POST['notification_group_id']) ? (int)$_POST['notification_group_id'] : null;
|
2025-10-12 12:46:16 +03:00
|
|
|
$tagsInput = trim($_POST['tags'] ?? '');
|
2025-10-20 19:05:33 +03:00
|
|
|
$userId = \Core\Auth::id();
|
2025-10-08 14:23:07 +03:00
|
|
|
|
|
|
|
|
// Validate
|
|
|
|
|
if (empty($domainName)) {
|
|
|
|
|
$_SESSION['error'] = 'Domain name is required';
|
|
|
|
|
$this->redirect('/domains/create');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
Add CSRF, CAPTCHA, and input validation improvements
Introduces CSRF protection to all sensitive controller actions, integrates configurable CAPTCHA (reCAPTCHA v2/v3, Turnstile) for authentication and registration flows, and centralizes input validation via a new InputValidator helper. Adds new helpers and services for CSRF and CAPTCHA, updates settings and migration for CAPTCHA configuration, and enhances logging and error handling in TLD registry import processes. Also improves validation for user, domain, group, and profile inputs throughout the application.
2025-10-10 00:04:12 +03:00
|
|
|
// Validate domain format
|
|
|
|
|
if (!\App\Helpers\InputValidator::validateDomain($domainName)) {
|
|
|
|
|
$_SESSION['error'] = 'Invalid domain name format (e.g., example.com)';
|
|
|
|
|
$this->redirect('/domains/create');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-12 12:46:16 +03:00
|
|
|
// Validate tags
|
|
|
|
|
$tagValidation = \App\Helpers\InputValidator::validateTags($tagsInput);
|
|
|
|
|
if (!$tagValidation['valid']) {
|
|
|
|
|
$_SESSION['error'] = $tagValidation['error'];
|
|
|
|
|
$this->redirect('/domains/create');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
$tags = $tagValidation['tags'];
|
|
|
|
|
|
2025-10-20 19:02:56 +03:00
|
|
|
// Validate notification group in isolation mode
|
|
|
|
|
if ($groupId) {
|
|
|
|
|
$settingModel = new \App\Models\Setting();
|
|
|
|
|
$isolationMode = $settingModel->getValue('user_isolation_mode', 'shared');
|
|
|
|
|
|
|
|
|
|
if ($isolationMode === 'isolated') {
|
|
|
|
|
$group = $this->groupModel->find($groupId);
|
|
|
|
|
if (!$group || $group['user_id'] != $userId) {
|
|
|
|
|
$_SESSION['error'] = 'You can only assign domains to your own notification groups';
|
|
|
|
|
$this->redirect('/domains/create');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-08 14:23:07 +03:00
|
|
|
// Check if domain already exists
|
|
|
|
|
if ($this->domainModel->existsByDomain($domainName)) {
|
|
|
|
|
$_SESSION['error'] = 'Domain already exists';
|
|
|
|
|
$this->redirect('/domains/create');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get WHOIS information
|
|
|
|
|
$whoisData = $this->whoisService->getDomainInfo($domainName);
|
|
|
|
|
|
|
|
|
|
if (!$whoisData) {
|
|
|
|
|
$_SESSION['error'] = 'Could not retrieve WHOIS information for this domain';
|
|
|
|
|
$this->redirect('/domains/create');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create domain
|
|
|
|
|
$status = $this->whoisService->getDomainStatus($whoisData['expiration_date'], $whoisData['status'] ?? []);
|
|
|
|
|
|
|
|
|
|
// Warn if domain is available (not registered)
|
|
|
|
|
if ($status === 'available') {
|
|
|
|
|
$_SESSION['warning'] = "Note: '$domainName' appears to be AVAILABLE (not registered). You're monitoring an unregistered domain.";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$id = $this->domainModel->create([
|
|
|
|
|
'domain_name' => $domainName,
|
|
|
|
|
'notification_group_id' => $groupId,
|
|
|
|
|
'registrar' => $whoisData['registrar'],
|
|
|
|
|
'registrar_url' => $whoisData['registrar_url'] ?? null,
|
|
|
|
|
'expiration_date' => $whoisData['expiration_date'],
|
|
|
|
|
'updated_date' => $whoisData['updated_date'] ?? null,
|
|
|
|
|
'abuse_email' => $whoisData['abuse_email'] ?? null,
|
|
|
|
|
'last_checked' => date('Y-m-d H:i:s'),
|
|
|
|
|
'status' => $status,
|
|
|
|
|
'whois_data' => json_encode($whoisData),
|
2025-10-20 19:02:56 +03:00
|
|
|
'is_active' => 1,
|
|
|
|
|
'user_id' => $userId
|
|
|
|
|
]);
|
|
|
|
|
|
2025-10-25 12:40:47 +03:00
|
|
|
// Handle tags using the new tag system
|
|
|
|
|
if (!empty($tags)) {
|
|
|
|
|
$tagModel = new \App\Models\Tag();
|
|
|
|
|
$tagModel->updateDomainTags($id, $tags, $userId);
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-20 19:02:56 +03:00
|
|
|
// Log domain creation
|
|
|
|
|
$logger = new \App\Services\Logger();
|
|
|
|
|
$logger->info('Domain created', [
|
|
|
|
|
'domain_id' => $id,
|
|
|
|
|
'domain_name' => $domainName,
|
|
|
|
|
'user_id' => $userId,
|
|
|
|
|
'status' => $status,
|
|
|
|
|
'expiration_date' => $whoisData['expiration_date'],
|
|
|
|
|
'notification_group_id' => $groupId
|
2025-10-08 14:23:07 +03:00
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
if ($status !== 'available') {
|
|
|
|
|
$_SESSION['success'] = "Domain '$domainName' added successfully";
|
|
|
|
|
}
|
|
|
|
|
$this->redirect('/domains');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function edit($params = [])
|
|
|
|
|
{
|
|
|
|
|
$id = $params['id'] ?? 0;
|
2025-10-25 02:04:00 +03:00
|
|
|
|
|
|
|
|
// Get current user and isolation mode
|
|
|
|
|
$userId = \Core\Auth::id();
|
|
|
|
|
$settingModel = new \App\Models\Setting();
|
|
|
|
|
$isolationMode = $settingModel->getValue('user_isolation_mode', 'shared');
|
|
|
|
|
|
|
|
|
|
// Get domain with tags and groups
|
|
|
|
|
if ($isolationMode === 'isolated') {
|
|
|
|
|
$domain = $this->domainModel->getWithTagsAndGroups($id, $userId);
|
|
|
|
|
} else {
|
|
|
|
|
$domain = $this->domainModel->getWithTagsAndGroups($id);
|
|
|
|
|
}
|
2025-10-08 14:23:07 +03:00
|
|
|
|
|
|
|
|
if (!$domain) {
|
|
|
|
|
$_SESSION['error'] = 'Domain not found';
|
|
|
|
|
$this->redirect('/domains');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-20 18:50:32 +03:00
|
|
|
// Get groups based on isolation mode
|
|
|
|
|
if ($isolationMode === 'isolated') {
|
|
|
|
|
$groups = $this->groupModel->getAllWithChannelCount($userId);
|
|
|
|
|
} else {
|
|
|
|
|
$groups = $this->groupModel->getAllWithChannelCount();
|
|
|
|
|
}
|
2025-10-25 02:04:00 +03:00
|
|
|
|
|
|
|
|
// Get available tags for the new tag system
|
|
|
|
|
$tagModel = new \App\Models\Tag();
|
|
|
|
|
if ($isolationMode === 'isolated') {
|
|
|
|
|
$availableTags = $tagModel->getAllWithUsage($userId);
|
|
|
|
|
} else {
|
|
|
|
|
$availableTags = $tagModel->getAllWithUsage();
|
|
|
|
|
}
|
2025-10-08 14:23:07 +03:00
|
|
|
|
2025-10-25 02:04:00 +03:00
|
|
|
// Get referrer for cancel button
|
|
|
|
|
$referrer = $_GET['from'] ?? '/domains/' . $domain['id'];
|
|
|
|
|
|
2025-10-08 14:23:07 +03:00
|
|
|
$this->view('domains/edit', [
|
|
|
|
|
'domain' => $domain,
|
|
|
|
|
'groups' => $groups,
|
2025-10-25 02:04:00 +03:00
|
|
|
'availableTags' => $availableTags,
|
|
|
|
|
'referrer' => $referrer,
|
2025-10-08 14:23:07 +03:00
|
|
|
'title' => 'Edit Domain'
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function update($params = [])
|
|
|
|
|
{
|
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
|
|
|
|
$this->redirect('/domains');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
Add CSRF, CAPTCHA, and input validation improvements
Introduces CSRF protection to all sensitive controller actions, integrates configurable CAPTCHA (reCAPTCHA v2/v3, Turnstile) for authentication and registration flows, and centralizes input validation via a new InputValidator helper. Adds new helpers and services for CSRF and CAPTCHA, updates settings and migration for CAPTCHA configuration, and enhances logging and error handling in TLD registry import processes. Also improves validation for user, domain, group, and profile inputs throughout the application.
2025-10-10 00:04:12 +03:00
|
|
|
// CSRF Protection
|
|
|
|
|
$this->verifyCsrf('/domains');
|
|
|
|
|
|
2025-10-08 14:23:07 +03:00
|
|
|
$id = (int)($params['id'] ?? 0);
|
2025-10-20 21:08:09 +03:00
|
|
|
$domain = $this->checkDomainAccess($id);
|
2025-10-08 14:23:07 +03:00
|
|
|
|
|
|
|
|
if (!$domain) {
|
|
|
|
|
$_SESSION['error'] = 'Domain not found';
|
|
|
|
|
$this->redirect('/domains');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-22 18:59:05 +03:00
|
|
|
$userId = \Core\Auth::id();
|
|
|
|
|
|
2025-10-08 14:23:07 +03:00
|
|
|
$groupId = !empty($_POST['notification_group_id']) ? (int)$_POST['notification_group_id'] : null;
|
|
|
|
|
$isActive = isset($_POST['is_active']) ? 1 : 0;
|
2025-10-12 12:46:16 +03:00
|
|
|
$tagsInput = trim($_POST['tags'] ?? '');
|
2025-10-21 16:13:58 +03:00
|
|
|
$manualExpirationDate = !empty($_POST['manual_expiration_date']) ? $_POST['manual_expiration_date'] : null;
|
2025-10-12 12:46:16 +03:00
|
|
|
|
|
|
|
|
// Validate tags
|
|
|
|
|
$tagValidation = \App\Helpers\InputValidator::validateTags($tagsInput);
|
|
|
|
|
if (!$tagValidation['valid']) {
|
|
|
|
|
$_SESSION['error'] = $tagValidation['error'];
|
|
|
|
|
$this->redirect('/domains/' . $id . '/edit');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
$tags = $tagValidation['tags'];
|
2025-10-20 19:02:56 +03:00
|
|
|
|
|
|
|
|
// Validate notification group in isolation mode
|
|
|
|
|
if ($groupId) {
|
|
|
|
|
$settingModel = new \App\Models\Setting();
|
|
|
|
|
$isolationMode = $settingModel->getValue('user_isolation_mode', 'shared');
|
|
|
|
|
|
|
|
|
|
if ($isolationMode === 'isolated') {
|
|
|
|
|
$group = $this->groupModel->find($groupId);
|
|
|
|
|
if (!$group || $group['user_id'] != $userId) {
|
|
|
|
|
$_SESSION['error'] = 'You can only assign domains to your own notification groups';
|
|
|
|
|
$this->redirect('/domains/' . $id . '/edit');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-10-08 14:23:07 +03:00
|
|
|
|
|
|
|
|
// Check if monitoring status changed
|
|
|
|
|
$statusChanged = ($domain['is_active'] != $isActive);
|
|
|
|
|
$oldGroupId = $domain['notification_group_id'];
|
|
|
|
|
|
|
|
|
|
$this->domainModel->update($id, [
|
|
|
|
|
'notification_group_id' => $groupId,
|
2025-10-21 16:13:58 +03:00
|
|
|
'is_active' => $isActive,
|
|
|
|
|
'expiration_date' => $manualExpirationDate
|
2025-10-08 14:23:07 +03:00
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
// Send notification if monitoring status changed and has notification group
|
|
|
|
|
if ($statusChanged && $groupId) {
|
|
|
|
|
$notificationService = new \App\Services\NotificationService();
|
|
|
|
|
|
|
|
|
|
if ($isActive) {
|
|
|
|
|
// Monitoring activated
|
|
|
|
|
$message = "🟢 Domain monitoring has been ACTIVATED for {$domain['domain_name']}\n\n" .
|
|
|
|
|
"The domain will now be monitored regularly and you'll receive expiration alerts.";
|
|
|
|
|
$subject = "✅ Monitoring Activated: {$domain['domain_name']}";
|
|
|
|
|
} else {
|
|
|
|
|
// Monitoring deactivated
|
|
|
|
|
$message = "🔴 Domain monitoring has been DEACTIVATED for {$domain['domain_name']}\n\n" .
|
|
|
|
|
"You will no longer receive alerts for this domain until monitoring is re-enabled.";
|
|
|
|
|
$subject = "⏸️ Monitoring Paused: {$domain['domain_name']}";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$notificationService->sendToGroup($groupId, $subject, $message);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Also send notification if group changed and monitoring is active
|
|
|
|
|
if (!$statusChanged && $isActive && $oldGroupId != $groupId) {
|
|
|
|
|
$notificationService = new \App\Services\NotificationService();
|
|
|
|
|
|
|
|
|
|
if ($groupId) {
|
|
|
|
|
// Assigned to new group
|
|
|
|
|
$groupModel = new NotificationGroup();
|
|
|
|
|
$group = $groupModel->find($groupId);
|
|
|
|
|
$groupName = $group ? $group['name'] : 'Unknown Group';
|
|
|
|
|
|
|
|
|
|
$message = "🔔 Notification group updated for {$domain['domain_name']}\n\n" .
|
|
|
|
|
"This domain is now assigned to: {$groupName}\n" .
|
|
|
|
|
"You will receive expiration alerts through this notification group.";
|
|
|
|
|
$subject = "📬 Group Changed: {$domain['domain_name']}";
|
|
|
|
|
|
|
|
|
|
$notificationService->sendToGroup($groupId, $subject, $message);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-25 02:04:00 +03:00
|
|
|
// Handle tags using the new tag system
|
|
|
|
|
if (!empty($tags)) {
|
|
|
|
|
$tagModel = new \App\Models\Tag();
|
|
|
|
|
$tagModel->updateDomainTags($id, $tags, $userId);
|
|
|
|
|
} else {
|
|
|
|
|
// Remove all tags from domain
|
|
|
|
|
$tagModel = new \App\Models\Tag();
|
|
|
|
|
$tagModel->removeAllFromDomain($id);
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-08 14:23:07 +03:00
|
|
|
$_SESSION['success'] = 'Domain updated successfully';
|
|
|
|
|
$this->redirect('/domains/' . $id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function refresh($params = [])
|
|
|
|
|
{
|
|
|
|
|
$id = $params['id'] ?? 0;
|
2025-10-20 21:08:09 +03:00
|
|
|
$domain = $this->checkDomainAccess($id);
|
2025-10-08 14:23:07 +03:00
|
|
|
|
|
|
|
|
if (!$domain) {
|
|
|
|
|
$_SESSION['error'] = 'Domain not found';
|
|
|
|
|
$this->redirect('/domains');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-21 15:16:56 +03:00
|
|
|
// Log domain refresh start
|
|
|
|
|
$logger = new \App\Services\Logger();
|
|
|
|
|
$logger->info('Domain refresh started', [
|
|
|
|
|
'domain_id' => $id,
|
|
|
|
|
'domain_name' => $domain['domain_name'],
|
|
|
|
|
'user_id' => \Core\Auth::id(),
|
|
|
|
|
'ip_address' => $_SERVER['REMOTE_ADDR'] ?? 'unknown'
|
|
|
|
|
]);
|
|
|
|
|
|
2025-10-08 14:23:07 +03:00
|
|
|
// Get fresh WHOIS information
|
|
|
|
|
$whoisData = $this->whoisService->getDomainInfo($domain['domain_name']);
|
|
|
|
|
|
|
|
|
|
if (!$whoisData) {
|
2025-10-21 15:16:56 +03:00
|
|
|
$logger->error('Domain refresh failed - WHOIS data not retrieved', [
|
|
|
|
|
'domain_id' => $id,
|
|
|
|
|
'domain_name' => $domain['domain_name'],
|
|
|
|
|
'user_id' => \Core\Auth::id()
|
|
|
|
|
]);
|
|
|
|
|
|
2025-10-08 14:23:07 +03:00
|
|
|
$_SESSION['error'] = 'Could not retrieve WHOIS information';
|
|
|
|
|
// Check if we came from view page
|
|
|
|
|
$referer = $_SERVER['HTTP_REFERER'] ?? '';
|
|
|
|
|
if (strpos($referer, '/domains/' . $id) !== false) {
|
|
|
|
|
$this->redirect('/domains/' . $id);
|
|
|
|
|
} else {
|
|
|
|
|
$this->redirect('/domains');
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-21 16:13:58 +03:00
|
|
|
// Use WHOIS expiration date if available, otherwise preserve manual expiration date
|
|
|
|
|
$expirationDate = $whoisData['expiration_date'] ?? $domain['expiration_date'];
|
|
|
|
|
|
|
|
|
|
$status = $this->whoisService->getDomainStatus($expirationDate, $whoisData['status'] ?? []);
|
2025-10-08 14:23:07 +03:00
|
|
|
|
|
|
|
|
$this->domainModel->update($id, [
|
|
|
|
|
'registrar' => $whoisData['registrar'],
|
|
|
|
|
'registrar_url' => $whoisData['registrar_url'] ?? null,
|
2025-10-21 16:13:58 +03:00
|
|
|
'expiration_date' => $expirationDate,
|
2025-10-08 14:23:07 +03:00
|
|
|
'updated_date' => $whoisData['updated_date'] ?? null,
|
|
|
|
|
'abuse_email' => $whoisData['abuse_email'] ?? null,
|
|
|
|
|
'last_checked' => date('Y-m-d H:i:s'),
|
|
|
|
|
'status' => $status,
|
|
|
|
|
'whois_data' => json_encode($whoisData)
|
|
|
|
|
]);
|
|
|
|
|
|
2025-10-21 15:16:56 +03:00
|
|
|
// Log successful domain refresh
|
|
|
|
|
$logger->info('Domain refresh completed successfully', [
|
|
|
|
|
'domain_id' => $id,
|
|
|
|
|
'domain_name' => $domain['domain_name'],
|
|
|
|
|
'new_status' => $status,
|
|
|
|
|
'registrar' => $whoisData['registrar'],
|
|
|
|
|
'expiration_date' => $whoisData['expiration_date'],
|
|
|
|
|
'user_id' => \Core\Auth::id()
|
|
|
|
|
]);
|
|
|
|
|
|
2025-10-08 14:23:07 +03:00
|
|
|
$_SESSION['success'] = 'Domain information refreshed';
|
|
|
|
|
|
|
|
|
|
// Check if we came from view page or list page
|
|
|
|
|
$referer = $_SERVER['HTTP_REFERER'] ?? '';
|
|
|
|
|
if (strpos($referer, '/domains/' . $id) !== false) {
|
|
|
|
|
// Came from view page, go back to view page
|
|
|
|
|
$this->redirect('/domains/' . $id);
|
|
|
|
|
} else {
|
|
|
|
|
// Came from list page, stay on list page
|
|
|
|
|
$this->redirect('/domains');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function delete($params = [])
|
|
|
|
|
{
|
|
|
|
|
$id = $params['id'] ?? 0;
|
2025-10-20 21:08:09 +03:00
|
|
|
$domain = $this->checkDomainAccess($id);
|
2025-10-08 14:23:07 +03:00
|
|
|
|
|
|
|
|
if (!$domain) {
|
|
|
|
|
$_SESSION['error'] = 'Domain not found';
|
|
|
|
|
$this->redirect('/domains');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$this->domainModel->delete($id);
|
|
|
|
|
$_SESSION['success'] = 'Domain deleted successfully';
|
|
|
|
|
$this->redirect('/domains');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function show($params = [])
|
|
|
|
|
{
|
|
|
|
|
$id = $params['id'] ?? 0;
|
2025-10-20 21:08:09 +03:00
|
|
|
|
|
|
|
|
// Get current user and isolation mode
|
|
|
|
|
$userId = \Core\Auth::id();
|
|
|
|
|
$settingModel = new \App\Models\Setting();
|
|
|
|
|
$isolationMode = $settingModel->getValue('user_isolation_mode', 'shared');
|
|
|
|
|
|
2025-10-25 02:04:00 +03:00
|
|
|
// Get domain with tags and groups
|
2025-10-20 21:08:09 +03:00
|
|
|
if ($isolationMode === 'isolated') {
|
2025-10-25 02:04:00 +03:00
|
|
|
$domain = $this->domainModel->getWithTagsAndGroups($id, $userId);
|
2025-10-20 21:08:09 +03:00
|
|
|
} else {
|
2025-10-25 02:04:00 +03:00
|
|
|
$domain = $this->domainModel->getWithTagsAndGroups($id);
|
2025-10-20 21:08:09 +03:00
|
|
|
}
|
2025-10-08 14:23:07 +03:00
|
|
|
|
|
|
|
|
if (!$domain) {
|
|
|
|
|
$_SESSION['error'] = 'Domain not found';
|
|
|
|
|
$this->redirect('/domains');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$logModel = new \App\Models\NotificationLog();
|
|
|
|
|
$logs = $logModel->getByDomain($id, 20);
|
Upgraded to 1.1.0
1.1.0 (2025-10-09)
- **User Notifications System** - In-app notification center with 7 notification types, filtering, pagination
- **Advanced Session Management** - Database-backed sessions with geolocation (country, city, ISP)
- **Remote Session Control** - Terminate any device instantly with immediate logout validation
- **Enhanced Profile Page** - Sidebar navigation with 4 tabs, hash-based routing (#profile, #security, #sessions)
- **MVC Architecture Refactoring** - 3 new Helpers (Layout, Domain, Session), ~265 lines cleaned from views
- **Geolocation Tracking** - IP-based location detection using ip-api.com, country flags with flag-icons
- **Device Detection** - Browser & device type parsing (Chrome/Firefox/Safari, Desktop/Mobile/Tablet)
- **Auto-Detected Cron Paths** - Settings show actual installation paths (thanks @jadeops)
- **Welcome Notifications** - Sent to new users on registration or fresh install
- **Upgrade Notifications** - Admins notified on system updates with version & migration count
- **Web-Based Installer** - Replaces CLI, auto-generates encryption key, one-time password display
- **Web-Based Updater** - `/install/update` for running new migrations with smart detection
- **User Registration** - Full signup flow with email verification, password reset, resend verification
- **User Management** - CRUD for users with filtering, sorting, pagination (admin-only)
- **Remember Me** - 30-day secure tokens linked to sessions, cascade deletion on logout
- **Session Validator** - Middleware validates sessions on every request for instant remote logout
- **Consistent UI/UX** - Unified filtering, sorting, pagination across Domains, Users, Notifications, TLD Registry
- **Smart Migrations** - Consolidated schema for fresh installs, incremental for upgrades
- **XSS Protection** - htmlspecialchars() applied across all user-facing data (thanks @jadeops)
2025-10-09 18:02:46 +03:00
|
|
|
|
|
|
|
|
// Format domain for display
|
|
|
|
|
$formattedDomain = \App\Helpers\DomainHelper::formatForDisplay($domain);
|
|
|
|
|
|
|
|
|
|
// Parse WHOIS data for display
|
|
|
|
|
$whoisData = json_decode($domain['whois_data'] ?? '{}', true);
|
|
|
|
|
if (!empty($whoisData['status']) && is_array($whoisData['status'])) {
|
|
|
|
|
$formattedDomain['parsedStatuses'] = \App\Helpers\DomainHelper::parseWhoisStatuses($whoisData['status']);
|
|
|
|
|
} else {
|
|
|
|
|
$formattedDomain['parsedStatuses'] = [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Calculate active channel count
|
|
|
|
|
if (!empty($domain['channels'])) {
|
|
|
|
|
$formattedDomain['activeChannelCount'] = \App\Helpers\DomainHelper::getActiveChannelCount($domain['channels']);
|
|
|
|
|
}
|
2025-10-25 02:04:00 +03:00
|
|
|
|
|
|
|
|
// Get available tags for the new tag system
|
|
|
|
|
$tagModel = new \App\Models\Tag();
|
|
|
|
|
if ($isolationMode === 'isolated') {
|
|
|
|
|
$availableTags = $tagModel->getAllWithUsage($userId);
|
|
|
|
|
} else {
|
|
|
|
|
$availableTags = $tagModel->getAllWithUsage();
|
|
|
|
|
}
|
2025-10-08 14:23:07 +03:00
|
|
|
|
|
|
|
|
$this->view('domains/view', [
|
Upgraded to 1.1.0
1.1.0 (2025-10-09)
- **User Notifications System** - In-app notification center with 7 notification types, filtering, pagination
- **Advanced Session Management** - Database-backed sessions with geolocation (country, city, ISP)
- **Remote Session Control** - Terminate any device instantly with immediate logout validation
- **Enhanced Profile Page** - Sidebar navigation with 4 tabs, hash-based routing (#profile, #security, #sessions)
- **MVC Architecture Refactoring** - 3 new Helpers (Layout, Domain, Session), ~265 lines cleaned from views
- **Geolocation Tracking** - IP-based location detection using ip-api.com, country flags with flag-icons
- **Device Detection** - Browser & device type parsing (Chrome/Firefox/Safari, Desktop/Mobile/Tablet)
- **Auto-Detected Cron Paths** - Settings show actual installation paths (thanks @jadeops)
- **Welcome Notifications** - Sent to new users on registration or fresh install
- **Upgrade Notifications** - Admins notified on system updates with version & migration count
- **Web-Based Installer** - Replaces CLI, auto-generates encryption key, one-time password display
- **Web-Based Updater** - `/install/update` for running new migrations with smart detection
- **User Registration** - Full signup flow with email verification, password reset, resend verification
- **User Management** - CRUD for users with filtering, sorting, pagination (admin-only)
- **Remember Me** - 30-day secure tokens linked to sessions, cascade deletion on logout
- **Session Validator** - Middleware validates sessions on every request for instant remote logout
- **Consistent UI/UX** - Unified filtering, sorting, pagination across Domains, Users, Notifications, TLD Registry
- **Smart Migrations** - Consolidated schema for fresh installs, incremental for upgrades
- **XSS Protection** - htmlspecialchars() applied across all user-facing data (thanks @jadeops)
2025-10-09 18:02:46 +03:00
|
|
|
'domain' => $formattedDomain,
|
2025-10-08 14:23:07 +03:00
|
|
|
'logs' => $logs,
|
2025-10-25 02:04:00 +03:00
|
|
|
'availableTags' => $availableTags,
|
2025-10-08 14:23:07 +03:00
|
|
|
'title' => $domain['domain_name']
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function bulkAdd()
|
|
|
|
|
{
|
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
|
2025-10-20 18:50:32 +03:00
|
|
|
// Get groups based on isolation mode
|
|
|
|
|
$userId = \Core\Auth::id();
|
|
|
|
|
$settingModel = new \App\Models\Setting();
|
|
|
|
|
$isolationMode = $settingModel->getValue('user_isolation_mode', 'shared');
|
|
|
|
|
|
|
|
|
|
if ($isolationMode === 'isolated') {
|
|
|
|
|
$groups = $this->groupModel->getAllWithChannelCount($userId);
|
|
|
|
|
} else {
|
|
|
|
|
$groups = $this->groupModel->getAllWithChannelCount();
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-25 02:04:00 +03:00
|
|
|
// Get available tags for the new tag system
|
|
|
|
|
$tagModel = new \App\Models\Tag();
|
|
|
|
|
if ($isolationMode === 'isolated') {
|
|
|
|
|
$availableTags = $tagModel->getAllWithUsage($userId);
|
|
|
|
|
} else {
|
|
|
|
|
$availableTags = $tagModel->getAllWithUsage();
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-08 14:23:07 +03:00
|
|
|
$this->view('domains/bulk-add', [
|
|
|
|
|
'groups' => $groups,
|
2025-10-25 02:04:00 +03:00
|
|
|
'availableTags' => $availableTags,
|
2025-10-08 14:23:07 +03:00
|
|
|
'title' => 'Bulk Add Domains'
|
|
|
|
|
]);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
Add CSRF, CAPTCHA, and input validation improvements
Introduces CSRF protection to all sensitive controller actions, integrates configurable CAPTCHA (reCAPTCHA v2/v3, Turnstile) for authentication and registration flows, and centralizes input validation via a new InputValidator helper. Adds new helpers and services for CSRF and CAPTCHA, updates settings and migration for CAPTCHA configuration, and enhances logging and error handling in TLD registry import processes. Also improves validation for user, domain, group, and profile inputs throughout the application.
2025-10-10 00:04:12 +03:00
|
|
|
// CSRF Protection
|
|
|
|
|
$this->verifyCsrf('/domains/bulk-add');
|
|
|
|
|
|
2025-10-08 14:23:07 +03:00
|
|
|
// POST - Process bulk add
|
|
|
|
|
$domainsText = trim($_POST['domains'] ?? '');
|
|
|
|
|
$groupId = !empty($_POST['notification_group_id']) ? (int)$_POST['notification_group_id'] : null;
|
2025-10-12 12:46:16 +03:00
|
|
|
$tagsInput = trim($_POST['tags'] ?? '');
|
2025-10-20 19:05:33 +03:00
|
|
|
$userId = \Core\Auth::id();
|
2025-10-08 14:23:07 +03:00
|
|
|
|
|
|
|
|
if (empty($domainsText)) {
|
|
|
|
|
$_SESSION['error'] = 'Please enter at least one domain';
|
|
|
|
|
$this->redirect('/domains/bulk-add');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-12 12:46:16 +03:00
|
|
|
// Validate tags
|
|
|
|
|
$tagValidation = \App\Helpers\InputValidator::validateTags($tagsInput);
|
|
|
|
|
if (!$tagValidation['valid']) {
|
|
|
|
|
$_SESSION['error'] = $tagValidation['error'];
|
|
|
|
|
$this->redirect('/domains/bulk-add');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
$tags = $tagValidation['tags'];
|
|
|
|
|
|
2025-10-20 19:02:56 +03:00
|
|
|
// Validate notification group in isolation mode
|
|
|
|
|
if ($groupId) {
|
|
|
|
|
$settingModel = new \App\Models\Setting();
|
|
|
|
|
$isolationMode = $settingModel->getValue('user_isolation_mode', 'shared');
|
|
|
|
|
|
|
|
|
|
if ($isolationMode === 'isolated') {
|
|
|
|
|
$group = $this->groupModel->find($groupId);
|
|
|
|
|
if (!$group || $group['user_id'] != $userId) {
|
|
|
|
|
$_SESSION['error'] = 'You can only assign domains to your own notification groups';
|
|
|
|
|
$this->redirect('/domains/bulk-add');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-08 14:23:07 +03:00
|
|
|
// Split by new lines and clean
|
|
|
|
|
$domainNames = array_filter(array_map('trim', explode("\n", $domainsText)));
|
|
|
|
|
|
|
|
|
|
$added = 0;
|
|
|
|
|
$skipped = 0;
|
|
|
|
|
$availableCount = 0;
|
|
|
|
|
$errors = [];
|
2025-10-20 19:02:56 +03:00
|
|
|
$userId = \Core\Auth::id();
|
|
|
|
|
|
|
|
|
|
// Log bulk add start
|
|
|
|
|
$logger = new \App\Services\Logger();
|
|
|
|
|
$logger->info('Bulk domain add started', [
|
|
|
|
|
'user_id' => $userId,
|
|
|
|
|
'domain_count' => count($domainNames),
|
|
|
|
|
'notification_group_id' => $groupId,
|
|
|
|
|
'tags' => $tags
|
|
|
|
|
]);
|
2025-10-08 14:23:07 +03:00
|
|
|
|
|
|
|
|
foreach ($domainNames as $domainName) {
|
|
|
|
|
// Skip if already exists
|
|
|
|
|
if ($this->domainModel->existsByDomain($domainName)) {
|
|
|
|
|
$skipped++;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get WHOIS information
|
|
|
|
|
$whoisData = $this->whoisService->getDomainInfo($domainName);
|
|
|
|
|
|
|
|
|
|
if (!$whoisData) {
|
|
|
|
|
$errors[] = $domainName;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$status = $this->whoisService->getDomainStatus($whoisData['expiration_date'], $whoisData['status'] ?? []);
|
|
|
|
|
|
|
|
|
|
// Track available domains
|
|
|
|
|
if ($status === 'available') {
|
|
|
|
|
$availableCount++;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-25 12:40:47 +03:00
|
|
|
$domainId = $this->domainModel->create([
|
2025-10-08 14:23:07 +03:00
|
|
|
'domain_name' => $domainName,
|
|
|
|
|
'notification_group_id' => $groupId,
|
|
|
|
|
'registrar' => $whoisData['registrar'],
|
|
|
|
|
'registrar_url' => $whoisData['registrar_url'] ?? null,
|
|
|
|
|
'expiration_date' => $whoisData['expiration_date'],
|
|
|
|
|
'updated_date' => $whoisData['updated_date'] ?? null,
|
|
|
|
|
'abuse_email' => $whoisData['abuse_email'] ?? null,
|
|
|
|
|
'last_checked' => date('Y-m-d H:i:s'),
|
|
|
|
|
'status' => $status,
|
|
|
|
|
'whois_data' => json_encode($whoisData),
|
2025-10-20 19:02:56 +03:00
|
|
|
'is_active' => 1,
|
|
|
|
|
'user_id' => \Core\Auth::id()
|
2025-10-08 14:23:07 +03:00
|
|
|
]);
|
|
|
|
|
|
2025-10-25 12:40:47 +03:00
|
|
|
// Handle tags using the new tag system
|
|
|
|
|
if (!empty($tags) && $domainId) {
|
|
|
|
|
$tagModel = new \App\Models\Tag();
|
|
|
|
|
$tagModel->updateDomainTags($domainId, $tags, $userId);
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-08 14:23:07 +03:00
|
|
|
$added++;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-20 19:02:56 +03:00
|
|
|
// Log bulk add completion
|
|
|
|
|
$logger->info('Bulk domain add completed', [
|
|
|
|
|
'user_id' => $userId,
|
|
|
|
|
'added' => $added,
|
|
|
|
|
'skipped' => $skipped,
|
|
|
|
|
'errors' => count($errors),
|
|
|
|
|
'available_count' => $availableCount
|
|
|
|
|
]);
|
|
|
|
|
|
2025-10-08 14:23:07 +03:00
|
|
|
$message = "Added $added domain(s)";
|
|
|
|
|
if ($skipped > 0) $message .= ", skipped $skipped duplicate(s)";
|
|
|
|
|
if (count($errors) > 0) $message .= ", failed to add " . count($errors) . " domain(s)";
|
|
|
|
|
|
|
|
|
|
if ($availableCount > 0) {
|
|
|
|
|
$_SESSION['warning'] = "Note: $availableCount domain(s) appear to be AVAILABLE (not registered).";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$_SESSION['success'] = $message;
|
|
|
|
|
$this->redirect('/domains');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function bulkRefresh()
|
|
|
|
|
{
|
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
|
|
|
|
$this->redirect('/domains');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
Add CSRF, CAPTCHA, and input validation improvements
Introduces CSRF protection to all sensitive controller actions, integrates configurable CAPTCHA (reCAPTCHA v2/v3, Turnstile) for authentication and registration flows, and centralizes input validation via a new InputValidator helper. Adds new helpers and services for CSRF and CAPTCHA, updates settings and migration for CAPTCHA configuration, and enhances logging and error handling in TLD registry import processes. Also improves validation for user, domain, group, and profile inputs throughout the application.
2025-10-10 00:04:12 +03:00
|
|
|
// CSRF Protection
|
|
|
|
|
$this->verifyCsrf('/domains');
|
|
|
|
|
|
2025-10-08 14:23:07 +03:00
|
|
|
$domainIds = $_POST['domain_ids'] ?? [];
|
|
|
|
|
|
|
|
|
|
if (empty($domainIds)) {
|
|
|
|
|
$_SESSION['error'] = 'No domains selected';
|
|
|
|
|
$this->redirect('/domains');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
Add CSRF, CAPTCHA, and input validation improvements
Introduces CSRF protection to all sensitive controller actions, integrates configurable CAPTCHA (reCAPTCHA v2/v3, Turnstile) for authentication and registration flows, and centralizes input validation via a new InputValidator helper. Adds new helpers and services for CSRF and CAPTCHA, updates settings and migration for CAPTCHA configuration, and enhances logging and error handling in TLD registry import processes. Also improves validation for user, domain, group, and profile inputs throughout the application.
2025-10-10 00:04:12 +03:00
|
|
|
// Validate bulk operation size
|
|
|
|
|
$sizeError = \App\Helpers\InputValidator::validateArraySize($domainIds, 1000, 'Domain selection');
|
|
|
|
|
if ($sizeError) {
|
|
|
|
|
$_SESSION['error'] = $sizeError;
|
|
|
|
|
$this->redirect('/domains');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-20 21:08:09 +03:00
|
|
|
// Get current user and isolation mode
|
|
|
|
|
$userId = \Core\Auth::id();
|
|
|
|
|
$settingModel = new \App\Models\Setting();
|
|
|
|
|
$isolationMode = $settingModel->getValue('user_isolation_mode', 'shared');
|
|
|
|
|
|
2025-10-21 15:16:56 +03:00
|
|
|
// Log bulk refresh start
|
|
|
|
|
$logger = new \App\Services\Logger();
|
|
|
|
|
$logger->info('Bulk domain refresh started', [
|
|
|
|
|
'user_id' => $userId,
|
|
|
|
|
'domain_count' => count($domainIds),
|
|
|
|
|
'isolation_mode' => $isolationMode,
|
|
|
|
|
'ip_address' => $_SERVER['REMOTE_ADDR'] ?? 'unknown'
|
|
|
|
|
]);
|
|
|
|
|
|
2025-10-08 14:23:07 +03:00
|
|
|
$refreshed = 0;
|
|
|
|
|
$failed = 0;
|
|
|
|
|
|
|
|
|
|
foreach ($domainIds as $id) {
|
2025-10-20 21:08:09 +03:00
|
|
|
// Check domain access based on isolation mode
|
|
|
|
|
if ($isolationMode === 'isolated') {
|
|
|
|
|
$domain = $this->domainModel->findWithIsolation($id, $userId);
|
|
|
|
|
} else {
|
|
|
|
|
$domain = $this->domainModel->find($id);
|
|
|
|
|
}
|
2025-10-08 14:23:07 +03:00
|
|
|
if (!$domain) continue;
|
|
|
|
|
|
|
|
|
|
$whoisData = $this->whoisService->getDomainInfo($domain['domain_name']);
|
|
|
|
|
|
|
|
|
|
if (!$whoisData) {
|
2025-10-21 15:16:56 +03:00
|
|
|
$logger->warning('Bulk refresh failed for domain - WHOIS data not retrieved', [
|
|
|
|
|
'domain_id' => $id,
|
|
|
|
|
'domain_name' => $domain['domain_name'] ?? 'unknown',
|
|
|
|
|
'user_id' => $userId
|
|
|
|
|
]);
|
2025-10-08 14:23:07 +03:00
|
|
|
$failed++;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-21 16:13:58 +03:00
|
|
|
// Use WHOIS expiration date if available, otherwise preserve manual expiration date
|
|
|
|
|
$expirationDate = $whoisData['expiration_date'] ?? $domain['expiration_date'];
|
|
|
|
|
|
|
|
|
|
$status = $this->whoisService->getDomainStatus($expirationDate, $whoisData['status'] ?? []);
|
2025-10-08 14:23:07 +03:00
|
|
|
|
|
|
|
|
$this->domainModel->update($id, [
|
|
|
|
|
'registrar' => $whoisData['registrar'],
|
|
|
|
|
'registrar_url' => $whoisData['registrar_url'] ?? null,
|
2025-10-21 16:13:58 +03:00
|
|
|
'expiration_date' => $expirationDate,
|
2025-10-08 14:23:07 +03:00
|
|
|
'updated_date' => $whoisData['updated_date'] ?? null,
|
|
|
|
|
'abuse_email' => $whoisData['abuse_email'] ?? null,
|
|
|
|
|
'last_checked' => date('Y-m-d H:i:s'),
|
|
|
|
|
'status' => $status,
|
|
|
|
|
'whois_data' => json_encode($whoisData)
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$refreshed++;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-21 15:16:56 +03:00
|
|
|
// Log bulk refresh completion
|
|
|
|
|
$logger->info('Bulk domain refresh completed', [
|
|
|
|
|
'user_id' => $userId,
|
|
|
|
|
'total_domains' => count($domainIds),
|
|
|
|
|
'refreshed' => $refreshed,
|
|
|
|
|
'failed' => $failed,
|
|
|
|
|
'success_rate' => count($domainIds) > 0 ? round(($refreshed / count($domainIds)) * 100, 2) . '%' : '0%'
|
|
|
|
|
]);
|
|
|
|
|
|
2025-10-08 14:23:07 +03:00
|
|
|
$_SESSION['success'] = "Refreshed $refreshed domain(s)" . ($failed > 0 ? ", $failed failed" : '');
|
|
|
|
|
$this->redirect('/domains');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function bulkDelete()
|
|
|
|
|
{
|
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
|
|
|
|
$this->redirect('/domains');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
Add CSRF, CAPTCHA, and input validation improvements
Introduces CSRF protection to all sensitive controller actions, integrates configurable CAPTCHA (reCAPTCHA v2/v3, Turnstile) for authentication and registration flows, and centralizes input validation via a new InputValidator helper. Adds new helpers and services for CSRF and CAPTCHA, updates settings and migration for CAPTCHA configuration, and enhances logging and error handling in TLD registry import processes. Also improves validation for user, domain, group, and profile inputs throughout the application.
2025-10-10 00:04:12 +03:00
|
|
|
// CSRF Protection
|
|
|
|
|
$this->verifyCsrf('/domains');
|
|
|
|
|
|
2025-10-08 14:23:07 +03:00
|
|
|
$domainIds = $_POST['domain_ids'] ?? [];
|
|
|
|
|
|
|
|
|
|
if (empty($domainIds)) {
|
|
|
|
|
$_SESSION['error'] = 'No domains selected';
|
|
|
|
|
$this->redirect('/domains');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
Add CSRF, CAPTCHA, and input validation improvements
Introduces CSRF protection to all sensitive controller actions, integrates configurable CAPTCHA (reCAPTCHA v2/v3, Turnstile) for authentication and registration flows, and centralizes input validation via a new InputValidator helper. Adds new helpers and services for CSRF and CAPTCHA, updates settings and migration for CAPTCHA configuration, and enhances logging and error handling in TLD registry import processes. Also improves validation for user, domain, group, and profile inputs throughout the application.
2025-10-10 00:04:12 +03:00
|
|
|
// Validate bulk operation size
|
|
|
|
|
$sizeError = \App\Helpers\InputValidator::validateArraySize($domainIds, 1000, 'Domain selection');
|
|
|
|
|
if ($sizeError) {
|
|
|
|
|
$_SESSION['error'] = $sizeError;
|
|
|
|
|
$this->redirect('/domains');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-08 14:23:07 +03:00
|
|
|
$deleted = 0;
|
|
|
|
|
foreach ($domainIds as $id) {
|
|
|
|
|
if ($this->domainModel->delete($id)) {
|
|
|
|
|
$deleted++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$_SESSION['success'] = "Deleted $deleted domain(s)";
|
|
|
|
|
$this->redirect('/domains');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function bulkAssignGroup()
|
|
|
|
|
{
|
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
|
|
|
|
$this->redirect('/domains');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
Add CSRF, CAPTCHA, and input validation improvements
Introduces CSRF protection to all sensitive controller actions, integrates configurable CAPTCHA (reCAPTCHA v2/v3, Turnstile) for authentication and registration flows, and centralizes input validation via a new InputValidator helper. Adds new helpers and services for CSRF and CAPTCHA, updates settings and migration for CAPTCHA configuration, and enhances logging and error handling in TLD registry import processes. Also improves validation for user, domain, group, and profile inputs throughout the application.
2025-10-10 00:04:12 +03:00
|
|
|
// CSRF Protection
|
|
|
|
|
$this->verifyCsrf('/domains');
|
|
|
|
|
|
2025-10-08 14:23:07 +03:00
|
|
|
$domainIds = $_POST['domain_ids'] ?? [];
|
|
|
|
|
$groupId = !empty($_POST['group_id']) ? (int)$_POST['group_id'] : null;
|
2025-10-20 19:05:33 +03:00
|
|
|
$userId = \Core\Auth::id();
|
2025-10-08 14:23:07 +03:00
|
|
|
|
|
|
|
|
if (empty($domainIds)) {
|
|
|
|
|
$_SESSION['error'] = 'No domains selected';
|
|
|
|
|
$this->redirect('/domains');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
Add CSRF, CAPTCHA, and input validation improvements
Introduces CSRF protection to all sensitive controller actions, integrates configurable CAPTCHA (reCAPTCHA v2/v3, Turnstile) for authentication and registration flows, and centralizes input validation via a new InputValidator helper. Adds new helpers and services for CSRF and CAPTCHA, updates settings and migration for CAPTCHA configuration, and enhances logging and error handling in TLD registry import processes. Also improves validation for user, domain, group, and profile inputs throughout the application.
2025-10-10 00:04:12 +03:00
|
|
|
// Validate bulk operation size
|
|
|
|
|
$sizeError = \App\Helpers\InputValidator::validateArraySize($domainIds, 1000, 'Domain selection');
|
|
|
|
|
if ($sizeError) {
|
|
|
|
|
$_SESSION['error'] = $sizeError;
|
|
|
|
|
$this->redirect('/domains');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-20 19:02:56 +03:00
|
|
|
// Validate notification group in isolation mode
|
|
|
|
|
if ($groupId) {
|
|
|
|
|
$settingModel = new \App\Models\Setting();
|
|
|
|
|
$isolationMode = $settingModel->getValue('user_isolation_mode', 'shared');
|
|
|
|
|
|
|
|
|
|
if ($isolationMode === 'isolated') {
|
|
|
|
|
$group = $this->groupModel->find($groupId);
|
|
|
|
|
if (!$group || $group['user_id'] != $userId) {
|
|
|
|
|
$_SESSION['error'] = 'You can only assign domains to your own notification groups';
|
|
|
|
|
$this->redirect('/domains');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-08 14:23:07 +03:00
|
|
|
$updated = 0;
|
|
|
|
|
foreach ($domainIds as $id) {
|
|
|
|
|
if ($this->domainModel->update($id, ['notification_group_id' => $groupId])) {
|
|
|
|
|
$updated++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$_SESSION['success'] = "Updated $updated domain(s)";
|
|
|
|
|
$this->redirect('/domains');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function bulkToggleStatus()
|
|
|
|
|
{
|
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
|
|
|
|
$this->redirect('/domains');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
Add CSRF, CAPTCHA, and input validation improvements
Introduces CSRF protection to all sensitive controller actions, integrates configurable CAPTCHA (reCAPTCHA v2/v3, Turnstile) for authentication and registration flows, and centralizes input validation via a new InputValidator helper. Adds new helpers and services for CSRF and CAPTCHA, updates settings and migration for CAPTCHA configuration, and enhances logging and error handling in TLD registry import processes. Also improves validation for user, domain, group, and profile inputs throughout the application.
2025-10-10 00:04:12 +03:00
|
|
|
// CSRF Protection
|
|
|
|
|
$this->verifyCsrf('/domains');
|
|
|
|
|
|
2025-10-08 14:23:07 +03:00
|
|
|
$domainIds = $_POST['domain_ids'] ?? [];
|
|
|
|
|
$isActive = isset($_POST['is_active']) ? (int)$_POST['is_active'] : 1;
|
|
|
|
|
|
|
|
|
|
if (empty($domainIds)) {
|
|
|
|
|
$_SESSION['error'] = 'No domains selected';
|
|
|
|
|
$this->redirect('/domains');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
Add CSRF, CAPTCHA, and input validation improvements
Introduces CSRF protection to all sensitive controller actions, integrates configurable CAPTCHA (reCAPTCHA v2/v3, Turnstile) for authentication and registration flows, and centralizes input validation via a new InputValidator helper. Adds new helpers and services for CSRF and CAPTCHA, updates settings and migration for CAPTCHA configuration, and enhances logging and error handling in TLD registry import processes. Also improves validation for user, domain, group, and profile inputs throughout the application.
2025-10-10 00:04:12 +03:00
|
|
|
// Validate bulk operation size
|
|
|
|
|
$sizeError = \App\Helpers\InputValidator::validateArraySize($domainIds, 1000, 'Domain selection');
|
|
|
|
|
if ($sizeError) {
|
|
|
|
|
$_SESSION['error'] = $sizeError;
|
|
|
|
|
$this->redirect('/domains');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-20 21:08:09 +03:00
|
|
|
// Get current user and isolation mode
|
|
|
|
|
$userId = \Core\Auth::id();
|
|
|
|
|
$settingModel = new \App\Models\Setting();
|
|
|
|
|
$isolationMode = $settingModel->getValue('user_isolation_mode', 'shared');
|
|
|
|
|
|
2025-10-08 14:23:07 +03:00
|
|
|
$updated = 0;
|
|
|
|
|
foreach ($domainIds as $id) {
|
2025-10-20 21:08:09 +03:00
|
|
|
// Check domain access based on isolation mode
|
|
|
|
|
if ($isolationMode === 'isolated') {
|
|
|
|
|
$domain = $this->domainModel->findWithIsolation($id, $userId);
|
|
|
|
|
} else {
|
|
|
|
|
$domain = $this->domainModel->find($id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($domain && $this->domainModel->update($id, ['is_active' => $isActive])) {
|
2025-10-08 14:23:07 +03:00
|
|
|
$updated++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$status = $isActive ? 'enabled' : 'disabled';
|
|
|
|
|
$_SESSION['success'] = "Monitoring $status for $updated domain(s)";
|
|
|
|
|
$this->redirect('/domains');
|
|
|
|
|
}
|
2025-10-08 20:56:25 +03:00
|
|
|
|
|
|
|
|
public function updateNotes($params = [])
|
|
|
|
|
{
|
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
|
|
|
|
$this->redirect('/domains');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
Add CSRF, CAPTCHA, and input validation improvements
Introduces CSRF protection to all sensitive controller actions, integrates configurable CAPTCHA (reCAPTCHA v2/v3, Turnstile) for authentication and registration flows, and centralizes input validation via a new InputValidator helper. Adds new helpers and services for CSRF and CAPTCHA, updates settings and migration for CAPTCHA configuration, and enhances logging and error handling in TLD registry import processes. Also improves validation for user, domain, group, and profile inputs throughout the application.
2025-10-10 00:04:12 +03:00
|
|
|
// CSRF Protection
|
|
|
|
|
$this->verifyCsrf('/domains');
|
|
|
|
|
|
2025-10-08 20:56:25 +03:00
|
|
|
$id = (int)($params['id'] ?? 0);
|
2025-10-20 21:08:09 +03:00
|
|
|
$domain = $this->checkDomainAccess($id);
|
2025-10-08 20:56:25 +03:00
|
|
|
|
|
|
|
|
if (!$domain) {
|
|
|
|
|
$_SESSION['error'] = 'Domain not found';
|
|
|
|
|
$this->redirect('/domains');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$notes = $_POST['notes'] ?? '';
|
|
|
|
|
|
Add CSRF, CAPTCHA, and input validation improvements
Introduces CSRF protection to all sensitive controller actions, integrates configurable CAPTCHA (reCAPTCHA v2/v3, Turnstile) for authentication and registration flows, and centralizes input validation via a new InputValidator helper. Adds new helpers and services for CSRF and CAPTCHA, updates settings and migration for CAPTCHA configuration, and enhances logging and error handling in TLD registry import processes. Also improves validation for user, domain, group, and profile inputs throughout the application.
2025-10-10 00:04:12 +03:00
|
|
|
// Validate notes length
|
|
|
|
|
$lengthError = \App\Helpers\InputValidator::validateLength($notes, 5000, 'Notes');
|
|
|
|
|
if ($lengthError) {
|
|
|
|
|
$_SESSION['error'] = $lengthError;
|
|
|
|
|
$this->redirect('/domains/' . $id);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-08 20:56:25 +03:00
|
|
|
$this->domainModel->update($id, [
|
|
|
|
|
'notes' => $notes
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$_SESSION['success'] = 'Notes updated successfully';
|
|
|
|
|
$this->redirect('/domains/' . $id);
|
|
|
|
|
}
|
2025-10-12 12:46:16 +03:00
|
|
|
|
|
|
|
|
public function bulkAddTags()
|
|
|
|
|
{
|
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
|
|
|
|
$this->redirect('/domains');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// CSRF Protection
|
|
|
|
|
$this->verifyCsrf('/domains');
|
|
|
|
|
|
|
|
|
|
$domainIds = $_POST['domain_ids'] ?? [];
|
|
|
|
|
$tagToAdd = trim($_POST['tag'] ?? '');
|
|
|
|
|
|
|
|
|
|
if (empty($domainIds) || empty($tagToAdd)) {
|
|
|
|
|
$_SESSION['error'] = 'Invalid request';
|
|
|
|
|
$this->redirect('/domains');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Validate tag format
|
|
|
|
|
if (!preg_match('/^[a-z0-9-]+$/', $tagToAdd)) {
|
|
|
|
|
$_SESSION['error'] = 'Invalid tag format (use only letters, numbers, and hyphens)';
|
|
|
|
|
$this->redirect('/domains');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-20 21:08:09 +03:00
|
|
|
// Get current user and isolation mode
|
|
|
|
|
$userId = \Core\Auth::id();
|
|
|
|
|
$settingModel = new \App\Models\Setting();
|
|
|
|
|
$isolationMode = $settingModel->getValue('user_isolation_mode', 'shared');
|
|
|
|
|
|
2025-10-25 12:40:47 +03:00
|
|
|
// Initialize Tag model
|
|
|
|
|
$tagModel = new \App\Models\Tag();
|
|
|
|
|
|
|
|
|
|
// Find or create the tag
|
|
|
|
|
$tag = $tagModel->findByName($tagToAdd, $userId);
|
|
|
|
|
if (!$tag) {
|
|
|
|
|
// Create new tag
|
|
|
|
|
$tagId = $tagModel->create([
|
|
|
|
|
'name' => $tagToAdd,
|
|
|
|
|
'color' => 'bg-gray-100 text-gray-700 border-gray-300',
|
|
|
|
|
'description' => '',
|
|
|
|
|
'user_id' => $userId
|
|
|
|
|
]);
|
|
|
|
|
if (!$tagId) {
|
|
|
|
|
$_SESSION['error'] = 'Failed to create tag';
|
|
|
|
|
$this->redirect('/domains');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
$tagId = $tag['id'];
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-12 12:46:16 +03:00
|
|
|
$updated = 0;
|
|
|
|
|
foreach ($domainIds as $id) {
|
2025-10-20 21:08:09 +03:00
|
|
|
// Check domain access based on isolation mode
|
|
|
|
|
if ($isolationMode === 'isolated') {
|
|
|
|
|
$domain = $this->domainModel->findWithIsolation($id, $userId);
|
|
|
|
|
} else {
|
|
|
|
|
$domain = $this->domainModel->find($id);
|
|
|
|
|
}
|
2025-10-12 12:46:16 +03:00
|
|
|
if (!$domain) continue;
|
|
|
|
|
|
2025-10-25 12:40:47 +03:00
|
|
|
// Add tag to domain using Tag model
|
|
|
|
|
if ($tagModel->addToDomain($id, $tagId)) {
|
|
|
|
|
$updated++;
|
2025-10-12 12:46:16 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$_SESSION['success'] = "Tag '$tagToAdd' added to $updated domain(s)";
|
|
|
|
|
$this->redirect('/domains');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function bulkRemoveTags()
|
|
|
|
|
{
|
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
|
|
|
|
$this->redirect('/domains');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// CSRF Protection
|
|
|
|
|
$this->verifyCsrf('/domains');
|
|
|
|
|
|
|
|
|
|
$domainIds = $_POST['domain_ids'] ?? [];
|
|
|
|
|
|
|
|
|
|
if (empty($domainIds)) {
|
|
|
|
|
$_SESSION['error'] = 'No domains selected';
|
|
|
|
|
$this->redirect('/domains');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-20 21:08:09 +03:00
|
|
|
// Get current user and isolation mode
|
|
|
|
|
$userId = \Core\Auth::id();
|
|
|
|
|
$settingModel = new \App\Models\Setting();
|
|
|
|
|
$isolationMode = $settingModel->getValue('user_isolation_mode', 'shared');
|
|
|
|
|
|
2025-10-25 02:04:00 +03:00
|
|
|
$tagModel = new \App\Models\Tag();
|
2025-10-12 12:46:16 +03:00
|
|
|
$updated = 0;
|
|
|
|
|
foreach ($domainIds as $id) {
|
2025-10-20 21:08:09 +03:00
|
|
|
// Check domain access based on isolation mode
|
|
|
|
|
if ($isolationMode === 'isolated') {
|
|
|
|
|
$domain = $this->domainModel->findWithIsolation($id, $userId);
|
|
|
|
|
} else {
|
|
|
|
|
$domain = $this->domainModel->find($id);
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-25 02:04:00 +03:00
|
|
|
if ($domain && $tagModel->removeAllFromDomain($id)) {
|
2025-10-12 12:46:16 +03:00
|
|
|
$updated++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$_SESSION['success'] = "Tags removed from $updated domain(s)";
|
|
|
|
|
$this->redirect('/domains');
|
|
|
|
|
}
|
2025-10-20 17:04:13 +03:00
|
|
|
|
2025-10-25 02:04:00 +03:00
|
|
|
/**
|
|
|
|
|
* Bulk remove specific tag from domains
|
|
|
|
|
*/
|
|
|
|
|
public function bulkRemoveSpecificTag()
|
|
|
|
|
{
|
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
|
|
|
|
$this->redirect('/domains');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// CSRF Protection
|
|
|
|
|
$this->verifyCsrf('/domains');
|
|
|
|
|
|
|
|
|
|
$domainIds = $_POST['domain_ids'] ?? [];
|
|
|
|
|
$tagId = (int)($_POST['tag_id'] ?? 0);
|
|
|
|
|
|
|
|
|
|
if (empty($domainIds) || !$tagId) {
|
|
|
|
|
$_SESSION['error'] = 'Invalid request';
|
|
|
|
|
$this->redirect('/domains');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$tagModel = new \App\Models\Tag();
|
|
|
|
|
$tag = $tagModel->find($tagId);
|
|
|
|
|
if (!$tag) {
|
|
|
|
|
$_SESSION['error'] = 'Tag not found';
|
|
|
|
|
$this->redirect('/domains');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get current user and isolation mode
|
|
|
|
|
$userId = \Core\Auth::id();
|
|
|
|
|
$settingModel = new \App\Models\Setting();
|
|
|
|
|
$isolationMode = $settingModel->getValue('user_isolation_mode', 'shared');
|
|
|
|
|
|
|
|
|
|
$removed = 0;
|
|
|
|
|
foreach ($domainIds as $domainId) {
|
|
|
|
|
// Check domain access based on isolation mode
|
|
|
|
|
if ($isolationMode === 'isolated') {
|
|
|
|
|
$domain = $this->domainModel->findWithIsolation($domainId, $userId);
|
|
|
|
|
} else {
|
|
|
|
|
$domain = $this->domainModel->find($domainId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($domain && $tagModel->removeFromDomain($domainId, $tagId)) {
|
|
|
|
|
$removed++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$_SESSION['success'] = "Tag '{$tag['name']}' removed from $removed domain(s)";
|
|
|
|
|
$this->redirect('/domains');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Bulk assign existing tag to domains
|
|
|
|
|
*/
|
|
|
|
|
public function bulkAssignExistingTag()
|
|
|
|
|
{
|
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
|
|
|
|
$this->redirect('/domains');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// CSRF Protection
|
|
|
|
|
$this->verifyCsrf('/domains');
|
|
|
|
|
|
|
|
|
|
$domainIds = $_POST['domain_ids'] ?? [];
|
|
|
|
|
$tagId = (int)($_POST['tag_id'] ?? 0);
|
|
|
|
|
|
|
|
|
|
if (empty($domainIds) || !$tagId) {
|
|
|
|
|
$_SESSION['error'] = 'Invalid request';
|
|
|
|
|
$this->redirect('/domains');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$tagModel = new \App\Models\Tag();
|
|
|
|
|
$tag = $tagModel->find($tagId);
|
|
|
|
|
if (!$tag) {
|
|
|
|
|
$_SESSION['error'] = 'Tag not found';
|
|
|
|
|
$this->redirect('/domains');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get current user and isolation mode
|
|
|
|
|
$userId = \Core\Auth::id();
|
|
|
|
|
$settingModel = new \App\Models\Setting();
|
|
|
|
|
$isolationMode = $settingModel->getValue('user_isolation_mode', 'shared');
|
|
|
|
|
|
|
|
|
|
$added = 0;
|
|
|
|
|
foreach ($domainIds as $domainId) {
|
|
|
|
|
// Check domain access based on isolation mode
|
|
|
|
|
if ($isolationMode === 'isolated') {
|
|
|
|
|
$domain = $this->domainModel->findWithIsolation($domainId, $userId);
|
|
|
|
|
} else {
|
|
|
|
|
$domain = $this->domainModel->find($domainId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($domain && $tagModel->addToDomain($domainId, $tagId)) {
|
|
|
|
|
$added++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$_SESSION['success'] = "Tag '{$tag['name']}' added to $added domain(s)";
|
|
|
|
|
$this->redirect('/domains');
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-20 17:04:13 +03:00
|
|
|
/**
|
|
|
|
|
* Transfer domain to another user (Admin only)
|
|
|
|
|
*/
|
|
|
|
|
public function transfer()
|
|
|
|
|
{
|
|
|
|
|
\Core\Auth::requireAdmin();
|
|
|
|
|
|
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
|
|
|
|
$this->redirect('/domains');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$this->verifyCsrf('/domains');
|
|
|
|
|
|
|
|
|
|
$domainId = (int)($_POST['domain_id'] ?? 0);
|
|
|
|
|
$targetUserId = (int)($_POST['target_user_id'] ?? 0);
|
|
|
|
|
|
|
|
|
|
if (!$domainId || !$targetUserId) {
|
|
|
|
|
$_SESSION['error'] = 'Invalid domain or user selected';
|
|
|
|
|
$this->redirect('/domains');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Validate domain exists
|
|
|
|
|
$domain = $this->domainModel->find($domainId);
|
|
|
|
|
if (!$domain) {
|
|
|
|
|
$_SESSION['error'] = 'Domain not found';
|
|
|
|
|
$this->redirect('/domains');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Validate target user exists
|
|
|
|
|
$userModel = new \App\Models\User();
|
|
|
|
|
$targetUser = $userModel->find($targetUserId);
|
|
|
|
|
if (!$targetUser) {
|
|
|
|
|
$_SESSION['error'] = 'Target user not found';
|
|
|
|
|
$this->redirect('/domains');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// Transfer domain
|
|
|
|
|
$this->domainModel->update($domainId, ['user_id' => $targetUserId]);
|
|
|
|
|
|
|
|
|
|
$_SESSION['success'] = "Domain '{$domain['domain_name']}' transferred to {$targetUser['username']}";
|
|
|
|
|
} catch (\Exception $e) {
|
|
|
|
|
$_SESSION['error'] = 'Failed to transfer domain. Please try again.';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$this->redirect('/domains');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Bulk transfer domains to another user (Admin only)
|
|
|
|
|
*/
|
|
|
|
|
public function bulkTransfer()
|
|
|
|
|
{
|
|
|
|
|
\Core\Auth::requireAdmin();
|
|
|
|
|
|
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
|
|
|
|
$this->redirect('/domains');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$this->verifyCsrf('/domains');
|
|
|
|
|
|
|
|
|
|
$domainIds = $_POST['domain_ids'] ?? [];
|
|
|
|
|
$targetUserId = (int)($_POST['target_user_id'] ?? 0);
|
|
|
|
|
|
|
|
|
|
if (empty($domainIds) || !$targetUserId) {
|
|
|
|
|
$_SESSION['error'] = 'No domains selected or invalid user';
|
|
|
|
|
$this->redirect('/domains');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Validate target user exists
|
|
|
|
|
$userModel = new \App\Models\User();
|
|
|
|
|
$targetUser = $userModel->find($targetUserId);
|
|
|
|
|
if (!$targetUser) {
|
|
|
|
|
$_SESSION['error'] = 'Target user not found';
|
|
|
|
|
$this->redirect('/domains');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$transferred = 0;
|
|
|
|
|
foreach ($domainIds as $domainId) {
|
|
|
|
|
$domainId = (int)$domainId;
|
|
|
|
|
if ($domainId > 0) {
|
|
|
|
|
try {
|
|
|
|
|
$this->domainModel->update($domainId, ['user_id' => $targetUserId]);
|
|
|
|
|
$transferred++;
|
|
|
|
|
} catch (\Exception $e) {
|
|
|
|
|
// Continue with other domains
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$_SESSION['success'] = "$transferred domain(s) transferred to {$targetUser['username']}";
|
|
|
|
|
$this->redirect('/domains');
|
|
|
|
|
}
|
2025-10-25 02:04:00 +03:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get tags for specific domains (API endpoint)
|
|
|
|
|
*/
|
|
|
|
|
public function getTagsForDomains()
|
|
|
|
|
{
|
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
|
|
|
|
$this->json(['error' => 'Method not allowed'], 405);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get JSON input
|
|
|
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
|
|
|
|
|
|
|
|
if (!isset($input['domain_ids']) || !is_array($input['domain_ids'])) {
|
|
|
|
|
$this->json(['error' => 'Invalid domain IDs'], 400);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$domainIds = array_map('intval', $input['domain_ids']);
|
|
|
|
|
$userId = \Core\Auth::id();
|
|
|
|
|
$settingModel = new \App\Models\Setting();
|
|
|
|
|
$isolationMode = $settingModel->getValue('user_isolation_mode', 'shared');
|
|
|
|
|
|
|
|
|
|
// Get tags that are assigned to the specified domains
|
|
|
|
|
$tags = $this->domainModel->getTagsForDomains($domainIds, $isolationMode === 'isolated' ? $userId : null);
|
|
|
|
|
|
|
|
|
|
$this->json(['tags' => $tags]);
|
|
|
|
|
}
|
2025-10-08 14:23:07 +03:00
|
|
|
}
|
|
|
|
|
|