prepare("REPLACE INTO settings (key, value) VALUES (?, ?)"); // Loop through all possible keys from the form to save them $setting_keys = ['site_title', 'remember_me_duration', 'language', 'favicon_extractor_url', 'logo_height']; foreach ($setting_keys as $key) { if (isset($_POST[$key])) { $upsert_stmt->execute([$key, $_POST[$key]]); } } // Handle checkboxes separately, as they are not sent when unchecked $checkboxes = ['display_site_title', 'open_new_tab', 'dashboard_protected', 'show_search_bar']; foreach ($checkboxes as $key) { $upsert_stmt->execute([$key, isset($_POST[$key]) ? '1' : '0']); } // Handle file uploads moved from 'update_theme' $uploadDir = 'data/images/custom/'; if (!is_dir($uploadDir)) { mkdir($uploadDir, 0755, true); } $allowed_logo_ext = ['png', 'jpg', 'jpeg', 'gif', 'svg']; $allowed_favicon_ext = ['png', 'ico', 'svg']; // Handle Logo Upload if (isset($_FILES['site_logo']) && $_FILES['site_logo']['error'] === UPLOAD_ERR_OK) { $logo_ext = strtolower(pathinfo($_FILES['site_logo']['name'], PATHINFO_EXTENSION)); if (in_array($logo_ext, $allowed_logo_ext)) { // To avoid caching issues, append a timestamp to the filename $logo_filename = 'logo.' . $logo_ext; $logo_path = $uploadDir . $logo_filename; if (move_uploaded_file($_FILES['site_logo']['tmp_name'], $logo_path)) { $upsert_stmt->execute(['site_logo', $logo_path . '?v=' . time()]); } else { $_SESSION['flash_message'] = ['text' => 'Failed to move uploaded logo.', 'type' => 'danger']; } } else { $_SESSION['flash_message'] = ['text' => 'Invalid file type for logo.', 'type' => 'danger']; } } // Handle Favicon Upload if (isset($_FILES['site_favicon']) && $_FILES['site_favicon']['error'] === UPLOAD_ERR_OK) { $favicon_ext = strtolower(pathinfo($_FILES['site_favicon']['name'], PATHINFO_EXTENSION)); if (in_array($favicon_ext, $allowed_favicon_ext)) { $favicon_filename = 'favicon.' . $favicon_ext; $favicon_path = $uploadDir . $favicon_filename; // Check if it's an image file (not SVG or ICO) if (in_array($favicon_ext, ['png', 'jpg', 'jpeg', 'gif'])) { // Get image dimensions $image_info = getimagesize($_FILES['site_favicon']['tmp_name']); if ($image_info === false) { $_SESSION['flash_message'] = ['text' => 'Invalid image file for favicon.', 'type' => 'danger']; } else { $width = $image_info[0]; $height = $image_info[1]; // Validate dimensions (minimum 180x180, maximum 256x256) if ($width < 180 || $height < 180) { $_SESSION['flash_message'] = ['text' => 'Favicon must be at least 180x180 pixels for Apple Touch Icon compatibility.', 'type' => 'danger']; } elseif ($width > 256 || $height > 256) { // Resize if too large if (resizeImage($_FILES['site_favicon']['tmp_name'], $favicon_path, 256, 256, $favicon_ext)) { $upsert_stmt->execute(['site_favicon', $favicon_path . '?v=' . time()]); $_SESSION['flash_message'] = ['text' => 'Favicon uploaded and resized to 256x256 pixels for optimal Apple Touch Icon compatibility.', 'type' => 'success']; } else { $_SESSION['flash_message'] = ['text' => 'Failed to resize favicon.', 'type' => 'danger']; } } else { // Size is acceptable, just move the file if (move_uploaded_file($_FILES['site_favicon']['tmp_name'], $favicon_path)) { $upsert_stmt->execute(['site_favicon', $favicon_path . '?v=' . time()]); } else { $_SESSION['flash_message'] = ['text' => 'Failed to move uploaded favicon.', 'type' => 'danger']; } } } } else { // For SVG and ICO files, just move them without resizing if (move_uploaded_file($_FILES['site_favicon']['tmp_name'], $favicon_path)) { $upsert_stmt->execute(['site_favicon', $favicon_path . '?v=' . time()]); } else { $_SESSION['flash_message'] = ['text' => 'Failed to move uploaded favicon.', 'type' => 'danger']; } } } else { $_SESSION['flash_message'] = ['text' => 'Invalid file type for favicon.', 'type' => 'danger']; } } if (!isset($_SESSION['flash_message'])) { $_SESSION['flash_message'] = ['text' => translate('settings_save_success'), 'type' => 'success']; } } elseif ($action === 'update_user') { $username = trim($_POST['username'] ?? ''); $new_password = $_POST['new_password'] ?? ''; $confirm_password = $_POST['confirm_password'] ?? ''; $user_id = $_SESSION['user_id']; if (empty($username)) { $_SESSION['flash_message'] = ['text' => 'Username cannot be empty.', 'type' => 'warning']; } else { // Check if password needs to be updated if (!empty($new_password)) { if ($new_password === $confirm_password) { $hashed_password = password_hash($new_password, PASSWORD_DEFAULT); $stmt = $pdo->prepare("UPDATE users SET username = ?, password = ? WHERE id = ?"); $stmt->execute([$username, $hashed_password, $user_id]); $_SESSION['flash_message'] = ['text' => translate('settings_user_update_success'), 'type' => 'success']; $_SESSION['username'] = $username; // Update session username } else { $_SESSION['flash_message'] = ['text' => translate('settings_password_mismatch'), 'type' => 'danger']; } } else { // Update username only $stmt = $pdo->prepare("UPDATE users SET username = ? WHERE id = ?"); $stmt->execute([$username, $user_id]); $_SESSION['flash_message'] = ['text' => translate('settings_user_update_success'), 'type' => 'success']; $_SESSION['username'] = $username; // Update session username } } } elseif ($action === 'update_theme') { $upsert_stmt = $pdo->prepare("REPLACE INTO settings (key, value) VALUES (?, ?)"); $upload_error = false; // Handle file uploads $backgroundUploadDir = 'data/images/backgrounds/'; if (!is_dir($backgroundUploadDir)) { mkdir($backgroundUploadDir, 0755, true); } // Handle Background Image Upload if (isset($_FILES['background_image']) && $_FILES['background_image']['error'] === UPLOAD_ERR_OK) { $bg_info = getimagesize($_FILES['background_image']['tmp_name']); if ($bg_info !== false) { // It's an image // Find and delete any existing global background $existing_files = glob($backgroundUploadDir . 'global-bg.*'); foreach ($existing_files as $file_to_delete) { unlink($file_to_delete); } $bg_ext = strtolower(pathinfo($_FILES['background_image']['name'], PATHINFO_EXTENSION)); $bg_filename = 'global-bg.' . $bg_ext; $bg_path = $backgroundUploadDir . $bg_filename; if (move_uploaded_file($_FILES['background_image']['tmp_name'], $bg_path)) { // Save the new path with a cache-busting query string $_POST['background_image_url'] = $bg_path . '?v=' . time(); } else { $_SESSION['flash_message'] = ['text' => 'Failed to move uploaded background image.', 'type' => 'danger']; $upload_error = true; } } else { $_SESSION['flash_message'] = ['text' => 'Invalid file type for background. Please upload an image.', 'type' => 'danger']; $upload_error = true; } } $theme_keys = ['theme', 'background_image_url', 'background_brightness', 'background_saturation', 'pexels_api_key', 'custom_css', 'theme_transparency', 'custom_footer_html']; foreach ($theme_keys as $key) { if (isset($_POST[$key])) { $upsert_stmt->execute([$key, trim($_POST[$key])]); } } if (!$upload_error) { $_SESSION['flash_message'] = ['text' => 'Theme settings saved successfully.', 'type' => 'success']; } } elseif ($action === 'restore_backup') { if (isset($_FILES['backup_file']) && $_FILES['backup_file']['error'] === UPLOAD_ERR_OK) { $file = $_FILES['backup_file']['tmp_name']; $zip = new ZipArchive; if ($zip->open($file) === TRUE) { // Check for essential files/directories in the zip if ($zip->locateName('database/bookmarkly.db') !== false) { // Correct paths for scripts running in the root directory $dataDir = 'data'; $extractPath = 'data/'; // Important: ensure the old data is removed or backed up before extraction // A more robust solution would backup the current 'data' dir first. // Clear the data directory before restoring, only if it exists if (is_dir($dataDir)) { $iterator = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($dataDir, RecursiveDirectoryIterator::SKIP_DOTS), RecursiveIteratorIterator::CHILD_FIRST ); foreach ($iterator as $fileinfo) { if ($fileinfo->isDir()) { @rmdir($fileinfo->getRealPath()); } else { @unlink($fileinfo->getRealPath()); } } } // Ensure the target directory exists if (!is_dir($extractPath)) { mkdir($extractPath, 0755, true); } // Extract the new data $zip->extractTo($extractPath); $zip->close(); session_destroy(); $_SESSION['flash_message'] = ['text' => translate('settings_backup_restore_success'), 'type' => 'success']; header('Location: login.php'); exit; } else { $_SESSION['flash_message'] = ['text' => translate('settings_backup_restore_failed'), 'type' => 'danger']; } $zip->close(); } else { $_SESSION['flash_message'] = ['text' => translate('settings_backup_restore_failed'), 'type' => 'danger']; } } else { $_SESSION['flash_message'] = ['text' => translate('settings_backup_restore_failed'), 'type' => 'danger']; } } // Redirect back to settings page for all actions except a successful restore header('Location: settings.php'); exit; } // Fetch tabs for the new import form $tabs_stmt = $pdo->prepare("SELECT id, name FROM tabs"); $tabs_stmt->execute(); $tabs = $tabs_stmt->fetchAll(); require_once 'include/header.php'; ?>

>
Current logo: Current Logo
Apple Touch Icon Requirements:
• Minimum size: 180x180 pixels
• Maximum size: 256x256 pixels
• Supported formats: PNG, JPG, GIF, SVG, ICO
• Images larger than 256x256 will be automatically resized
• Images smaller than 180x180 will be rejected
Current favicon: Current Favicon

>
>
>

These settings serve as the global default. They can be overridden by individual tabs in the Tab Management page.


Current Background

>




System Maintenance

Update Database Schema

If new features require database changes, you can apply them here. This process is safe to run multiple times.



Fix Public Tabs Visibility

This will fix tabs that should be public but have empty string instead of NULL for user_id. This ensures that users and viewers can see public tabs properly.