2025-10-08 14:23:07 +03:00
|
|
|
<?php
|
|
|
|
|
$title = 'Bulk Add Domains';
|
|
|
|
|
$pageTitle = 'Bulk Add Domains';
|
|
|
|
|
$pageDescription = 'Add multiple domains at once';
|
|
|
|
|
$pageIcon = 'fas fa-layer-group';
|
|
|
|
|
ob_start();
|
|
|
|
|
?>
|
|
|
|
|
|
|
|
|
|
<!-- Main Form -->
|
|
|
|
|
<div class="max-w-4xl mx-auto">
|
|
|
|
|
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
|
|
|
|
|
<div class="px-6 py-4 border-b border-gray-200">
|
|
|
|
|
<h2 class="text-lg font-semibold text-gray-900 flex items-center">
|
|
|
|
|
<i class="fas fa-layer-group text-gray-400 mr-2 text-sm"></i>
|
|
|
|
|
Bulk Add Domains
|
|
|
|
|
</h2>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="p-6">
|
|
|
|
|
<form method="POST" action="/domains/bulk-add" class="space-y-5">
|
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_field() ?>
|
2025-10-08 14:23:07 +03:00
|
|
|
<!-- Domains Textarea -->
|
|
|
|
|
<div>
|
|
|
|
|
<label for="domains" class="block text-sm font-medium text-gray-700 mb-1.5">
|
|
|
|
|
Domain Names *
|
|
|
|
|
</label>
|
|
|
|
|
<textarea
|
|
|
|
|
id="domains"
|
|
|
|
|
name="domains"
|
|
|
|
|
rows="10"
|
|
|
|
|
class="w-full px-3 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary transition-colors text-sm font-mono"
|
|
|
|
|
placeholder="example.com google.com github.com ..."
|
|
|
|
|
required
|
|
|
|
|
autofocus></textarea>
|
|
|
|
|
<p class="mt-1.5 text-xs text-gray-500">
|
|
|
|
|
Enter one domain per line. Domains without http:// or www.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-10-12 12:46:16 +03:00
|
|
|
<!-- Tags -->
|
|
|
|
|
<div>
|
|
|
|
|
<label for="tags-input" class="block text-sm font-medium text-gray-700 mb-1.5">
|
|
|
|
|
Tags
|
|
|
|
|
<span class="text-gray-400 font-normal">(Optional)</span>
|
|
|
|
|
</label>
|
|
|
|
|
|
|
|
|
|
<!-- Tag Display Area -->
|
|
|
|
|
<div id="tags-display" class="min-h-[40px] p-2 border border-gray-300 rounded-lg mb-2 flex flex-wrap gap-1.5 bg-gray-50"></div>
|
|
|
|
|
|
|
|
|
|
<!-- Tag Input -->
|
|
|
|
|
<div class="relative">
|
|
|
|
|
<i class="fas fa-tag absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 text-sm"></i>
|
|
|
|
|
<input type="text"
|
|
|
|
|
id="tags-input"
|
|
|
|
|
class="w-full pl-10 pr-20 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary transition-colors text-sm"
|
|
|
|
|
placeholder="Type any tag and press Enter or comma..."
|
|
|
|
|
onkeydown="handleTagInput(event)">
|
|
|
|
|
<button type="button" onclick="addTagFromInput()" class="absolute right-2 top-1/2 transform -translate-y-1/2 px-3 py-1 bg-primary text-white text-xs rounded hover:bg-primary-dark">
|
|
|
|
|
Add
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Hidden input to store tags for form submission -->
|
|
|
|
|
<input type="hidden" id="tags" name="tags" value="">
|
|
|
|
|
|
|
|
|
|
<p class="mt-1.5 text-xs text-gray-500">
|
|
|
|
|
<i class="fas fa-info-circle mr-1"></i>
|
|
|
|
|
All imported domains will be tagged with these tags. Type any custom tag or use suggestions below.
|
|
|
|
|
</p>
|
|
|
|
|
|
2025-10-25 02:04:00 +03:00
|
|
|
<!-- Available Tags -->
|
2025-10-12 12:46:16 +03:00
|
|
|
<div class="mt-2">
|
2025-10-25 02:04:00 +03:00
|
|
|
<p class="text-xs text-gray-600 mb-1.5">💡 Available Tags:</p>
|
2025-10-12 12:46:16 +03:00
|
|
|
<div class="flex flex-wrap gap-1.5">
|
2025-10-25 02:04:00 +03:00
|
|
|
<?php foreach ($availableTags as $tag): ?>
|
|
|
|
|
<button type="button" onclick="addTag('<?= htmlspecialchars($tag['name']) ?>')"
|
|
|
|
|
class="inline-flex items-center px-2 py-1 rounded-md text-xs font-medium border <?= htmlspecialchars($tag['color']) ?> hover:opacity-80 transition-colors">
|
|
|
|
|
<i class="fas fa-plus mr-1" style="font-size: 8px;"></i>
|
|
|
|
|
<?= htmlspecialchars($tag['name']) ?>
|
|
|
|
|
</button>
|
|
|
|
|
<?php endforeach; ?>
|
2025-10-12 12:46:16 +03:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-10-08 14:23:07 +03:00
|
|
|
<!-- Notification Group -->
|
|
|
|
|
<div>
|
|
|
|
|
<label for="notification_group_id" class="block text-sm font-medium text-gray-700 mb-1.5">
|
|
|
|
|
Notification Group (Optional)
|
|
|
|
|
</label>
|
|
|
|
|
<select id="notification_group_id"
|
|
|
|
|
name="notification_group_id"
|
|
|
|
|
class="w-full px-3 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary transition-colors text-sm">
|
|
|
|
|
<option value="">-- No Group (No notifications) --</option>
|
|
|
|
|
<?php foreach ($groups as $group): ?>
|
|
|
|
|
<option value="<?= $group['id'] ?>"><?= htmlspecialchars($group['name']) ?></option>
|
|
|
|
|
<?php endforeach; ?>
|
|
|
|
|
</select>
|
|
|
|
|
<p class="mt-1.5 text-xs text-gray-500">
|
|
|
|
|
Assign all domains to this notification group
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Action Buttons -->
|
|
|
|
|
<div class="flex flex-col sm:flex-row gap-3 pt-3">
|
|
|
|
|
<button type="submit"
|
|
|
|
|
class="inline-flex items-center justify-center px-5 py-2.5 bg-primary hover:bg-primary-dark text-white rounded-lg font-medium transition-colors text-sm">
|
|
|
|
|
<i class="fas fa-plus-circle mr-2"></i>
|
|
|
|
|
Add All Domains
|
|
|
|
|
</button>
|
|
|
|
|
<a href="/domains"
|
|
|
|
|
class="inline-flex items-center justify-center px-5 py-2.5 border border-gray-300 text-gray-700 rounded-lg font-medium hover:bg-gray-50 transition-colors text-sm">
|
|
|
|
|
<i class="fas fa-times mr-2"></i>
|
|
|
|
|
Cancel
|
|
|
|
|
</a>
|
|
|
|
|
</div>
|
|
|
|
|
</form>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Info Cards -->
|
|
|
|
|
<div class="mt-4 grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
|
|
|
<!-- How it works -->
|
|
|
|
|
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
|
|
|
|
<div class="flex items-start">
|
|
|
|
|
<div class="flex-shrink-0">
|
|
|
|
|
<div class="w-10 h-10 bg-blue-500 rounded-lg flex items-center justify-center">
|
|
|
|
|
<i class="fas fa-info-circle text-white"></i>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="ml-3">
|
|
|
|
|
<h3 class="text-sm font-semibold text-gray-900 mb-1">How It Works</h3>
|
|
|
|
|
<p class="text-xs text-gray-600 leading-relaxed">
|
|
|
|
|
Paste multiple domain names, one per line. The system will fetch WHOIS information
|
|
|
|
|
for each domain automatically. This may take a few moments depending on how many domains you're adding.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Important notes -->
|
|
|
|
|
<div class="bg-orange-50 border border-orange-200 rounded-lg p-4">
|
|
|
|
|
<div class="flex items-start">
|
|
|
|
|
<div class="flex-shrink-0">
|
|
|
|
|
<div class="w-10 h-10 bg-orange-500 rounded-lg flex items-center justify-center">
|
|
|
|
|
<i class="fas fa-exclamation-triangle text-white"></i>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="ml-3">
|
|
|
|
|
<h3 class="text-sm font-semibold text-gray-900 mb-1">Important Notes</h3>
|
|
|
|
|
<ul class="text-xs text-gray-600 space-y-1">
|
|
|
|
|
<li class="flex items-start">
|
|
|
|
|
<i class="fas fa-circle text-orange-500 mt-1 mr-2" style="font-size: 6px;"></i>
|
|
|
|
|
<span>Duplicate domains will be skipped</span>
|
|
|
|
|
</li>
|
|
|
|
|
<li class="flex items-start">
|
|
|
|
|
<i class="fas fa-circle text-orange-500 mt-1 mr-2" style="font-size: 6px;"></i>
|
|
|
|
|
<span>Invalid domains will be reported</span>
|
|
|
|
|
</li>
|
|
|
|
|
<li class="flex items-start">
|
|
|
|
|
<i class="fas fa-circle text-orange-500 mt-1 mr-2" style="font-size: 6px;"></i>
|
|
|
|
|
<span>Large batches may take several minutes</span>
|
|
|
|
|
</li>
|
|
|
|
|
</ul>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-10-12 12:46:16 +03:00
|
|
|
<script>
|
|
|
|
|
let tags = [];
|
|
|
|
|
|
2025-10-25 02:04:00 +03:00
|
|
|
// Available tags with their colors from the database
|
|
|
|
|
const availableTags = <?= json_encode($availableTags) ?>;
|
|
|
|
|
const tagColors = {};
|
|
|
|
|
availableTags.forEach(tag => {
|
|
|
|
|
tagColors[tag.name] = tag.color;
|
|
|
|
|
});
|
2025-10-12 12:46:16 +03:00
|
|
|
|
|
|
|
|
function addTag(tagName) {
|
|
|
|
|
tagName = tagName.trim().toLowerCase();
|
|
|
|
|
|
|
|
|
|
// Validate tag (alphanumeric and hyphens only)
|
|
|
|
|
if (!/^[a-z0-9-]+$/.test(tagName)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if tag already exists
|
|
|
|
|
if (tags.includes(tagName)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tags.push(tagName);
|
|
|
|
|
updateTagsDisplay();
|
|
|
|
|
updateHiddenInput();
|
|
|
|
|
|
|
|
|
|
// Clear input
|
|
|
|
|
document.getElementById('tags-input').value = '';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function removeTag(tagName) {
|
|
|
|
|
tags = tags.filter(t => t !== tagName);
|
|
|
|
|
updateTagsDisplay();
|
|
|
|
|
updateHiddenInput();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function updateTagsDisplay() {
|
|
|
|
|
const display = document.getElementById('tags-display');
|
|
|
|
|
display.innerHTML = '';
|
|
|
|
|
|
|
|
|
|
if (tags.length === 0) {
|
|
|
|
|
display.innerHTML = '<span class="text-xs text-gray-400 italic">No tags added yet</span>';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tags.forEach(tag => {
|
|
|
|
|
const colorClass = tagColors[tag] || 'bg-gray-100 text-gray-700 border-gray-300';
|
|
|
|
|
const tagElement = document.createElement('span');
|
|
|
|
|
tagElement.className = `inline-flex items-center px-2.5 py-1 rounded-md text-xs font-medium border ${colorClass}`;
|
|
|
|
|
tagElement.innerHTML = `
|
|
|
|
|
<i class="fas fa-tag mr-1" style="font-size: 9px;"></i>
|
|
|
|
|
${tag}
|
|
|
|
|
<button type="button" onclick="removeTag('${tag}')" class="ml-1.5 hover:text-red-600">
|
|
|
|
|
<i class="fas fa-times" style="font-size: 9px;"></i>
|
|
|
|
|
</button>
|
|
|
|
|
`;
|
|
|
|
|
display.appendChild(tagElement);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function updateHiddenInput() {
|
|
|
|
|
document.getElementById('tags').value = tags.join(',');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleTagInput(event) {
|
|
|
|
|
if (event.key === 'Enter' || event.key === ',') {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
addTagFromInput();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function addTagFromInput() {
|
|
|
|
|
const input = document.getElementById('tags-input');
|
|
|
|
|
const value = input.value.trim();
|
|
|
|
|
|
|
|
|
|
if (value) {
|
|
|
|
|
// Handle multiple tags separated by commas
|
|
|
|
|
const newTags = value.split(',').map(t => t.trim().toLowerCase()).filter(t => t);
|
|
|
|
|
newTags.forEach(tag => addTag(tag));
|
|
|
|
|
input.value = '';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Initialize display
|
|
|
|
|
updateTagsDisplay();
|
|
|
|
|
</script>
|
|
|
|
|
|
2025-10-08 14:23:07 +03:00
|
|
|
<?php
|
|
|
|
|
$content = ob_get_clean();
|
|
|
|
|
include __DIR__ . '/../layout/base.php';
|
|
|
|
|
?>
|
|
|
|
|
|