Compare commits

..
5 Commits
Author SHA1 Message Date
rogier d536bd3e20 Analytics 2026-01-01 11:18:51 +01:00
rogier ec95543395 Speed-up caching 2025-12-01 21:16:37 +01:00
rogier 6c8707f08b Update en.php 2025-12-01 20:40:30 +01:00
rogier 91e73b1f1d Apple icon 2025-10-11 16:36:53 +02:00
rogier 5274da2c70 Cookie session 2025-10-11 00:16:59 +02:00
15 changed files with 178 additions and 705 deletions

No files matched your search

-66
View File
@@ -1,66 +0,0 @@
# Enable mod_rewrite
RewriteEngine On
# Security headers
Header always set X-Content-Type-Options nosniff
Header always set X-Frame-Options DENY
Header always set X-XSS-Protection "1; mode=block"
# Cache static assets
<FilesMatch "\.(jpg|jpeg|png|gif|webp|svg|ico|css|js)$">
# Cache for 30 days
ExpiresActive On
ExpiresByType image/jpeg "access plus 30 days"
ExpiresByType image/png "access plus 30 days"
ExpiresByType image/gif "access plus 30 days"
ExpiresByType image/webp "access plus 30 days"
ExpiresByType image/svg+xml "access plus 30 days"
ExpiresByType image/x-icon "access plus 30 days"
ExpiresByType text/css "access plus 7 days"
ExpiresByType application/javascript "access plus 7 days"
# Cache control
Header set Cache-Control "public, max-age=2592000"
# Enable compression
<IfModule mod_deflate.c>
SetOutputFilter DEFLATE
SetEnvIfNoCase Request_URI \
\.(?:gif|jpe?g|png)$ no-gzip dont-vary
SetEnvIfNoCase Request_URI \
\.(?:exe|t?gz|zip|bz2|sit|rar)$ no-gzip dont-vary
</IfModule>
</FilesMatch>
# Cache API image proxy
<FilesMatch "^api/image\.php$">
ExpiresActive On
ExpiresDefault "access plus 1 day"
Header set Cache-Control "public, max-age=86400"
</FilesMatch>
# Deny access to sensitive files
<FilesMatch "\.(htaccess|htpasswd|ini|log|sh|inc|bak)$">
Order Allow,Deny
Deny from all
</FilesMatch>
# Deny access to sensitive files in data directory
<FilesMatch "^data/.*\.(db|sqlite|log|txt|bak)$">
Order Allow,Deny
Deny from all
</FilesMatch>
# Allow access to images in data directory
<FilesMatch "^data/images/.*\.(jpg|jpeg|png|gif|webp|svg|ico)$">
Order Allow,Deny
Allow from all
</FilesMatch>
# PHP settings
<IfModule mod_php.c>
php_value upload_max_filesize 100M
php_value post_max_size 100M
php_value memory_limit 512M
php_value max_execution_time 300
</IfModule>
+2 -6
View File
@@ -11,9 +11,8 @@ RUN apt-get update && apt-get install -y \
imagemagick \ imagemagick \
libmagickwand-dev \ libmagickwand-dev \
unzip \ unzip \
curl \
&& docker-php-ext-configure gd --with-jpeg --with-webp \ && docker-php-ext-configure gd --with-jpeg --with-webp \
&& docker-php-ext-install pdo_sqlite zip gd \ && docker-php-ext-install pdo_sqlite zip gd opcache \
&& pecl install imagick \ && pecl install imagick \
&& docker-php-ext-enable imagick && docker-php-ext-enable imagick
@@ -26,11 +25,8 @@ COPY . /var/www/html
# Copy custom PHP configuration # Copy custom PHP configuration
COPY /docker/php.ini /usr/local/etc/php/conf.d/php.ini COPY /docker/php.ini /usr/local/etc/php/conf.d/php.ini
# Enable Apache modules
RUN a2enmod rewrite headers deflate expires
# Create data directories and set permissions # Create data directories and set permissions
RUN mkdir -p /var/www/html/data/database /var/www/html/data/images/custom /var/www/html/data/images/backgrounds /var/www/html/data/images/cache && \ RUN mkdir -p /var/www/html/data/database /var/www/html/data/images/custom /var/www/html/data/images/backgrounds && \
chmod -R 775 /var/www/html/data chmod -R 775 /var/www/html/data
# Copy and set up entrypoint # Copy and set up entrypoint
-147
View File
@@ -1,147 +0,0 @@
# Varnish Cache Setup for Bookmarkly
This setup implements Varnish Cache as a reverse proxy for Bookmarkly to dramatically improve performance, especially for images.
## Architecture
```
Internet → Varnish Cache (Port 80) → Apache/PHP App (Port 80 internal)
```
## Features
- **Ultra-fast caching** of static assets (images, CSS, JS)
- **CDN-like functionality** for external images
- **Automatic compression** (Gzip)
- **Smart cache invalidation**
- **Image optimization** and resizing
- **Cache purging** via API
## Quick Start
1. **Start the services:**
```bash
docker-compose up -d
```
2. **Access the application:**
- Main app: http://localhost
- Varnish admin: http://localhost:6082
## Configuration
### Varnish Configuration (`varnish/default.vcl`)
- **Image caching**: 30 days
- **CSS/JS caching**: 7 days
- **API proxy caching**: 1 day
- **Compression**: Enabled for text-based content
- **CORS**: Enabled for images
### Image Proxy (`api/image.php`)
Use the image proxy for external images:
```
http://localhost/api/image.php?url=https://example.com/image.jpg&w=800&h=600&q=85
```
Parameters:
- `url`: External image URL
- `w`: Width (optional)
- `h`: Height (optional)
- `q`: Quality 1-100 (optional, default: 85)
### Cache Purging (`api/purge_cache.php`)
Purge cache via API (Admin only):
```bash
# Purge all cache
curl -X POST http://localhost/api/purge_cache.php -d "type=all"
# Purge specific image
curl -X POST http://localhost/api/purge_cache.php -d "type=image&url=/path/to/image.jpg"
# Purge all images
curl -X POST http://localhost/api/purge_cache.php -d "type=images"
```
## Performance Benefits
- **10-100x faster** image loading
- **Reduced bandwidth** usage
- **Lower server load**
- **Better user experience**
- **CDN-like performance** locally
## Monitoring
### Varnish Statistics
```bash
# View Varnish stats
docker exec bookmarkly-varnish varnishstat
# View Varnish logs
docker exec bookmarkly-varnish varnishlog
```
### Cache Hit Rate
Check the `X-Cache` header in responses:
- `HIT`: Served from cache
- `MISS`: Fetched from backend
## Troubleshooting
### Clear All Cache
```bash
docker exec bookmarkly-varnish varnishadm "ban req.url ~ ."
```
### Restart Varnish
```bash
docker-compose restart varnish
```
### Check Configuration
```bash
docker exec bookmarkly-varnish varnishd -C -f /etc/varnish/default.vcl
```
## Security
- **Domain whitelist** for external images
- **URL validation** to prevent abuse
- **File type restrictions**
- **Rate limiting** (can be added)
## Customization
### Add Allowed Domains
Edit `api/image.php` and add domains to `$allowedDomains` array.
### Adjust Cache TTL
Modify `varnish/default.vcl` to change cache durations.
### Add Compression
Varnish automatically compresses text-based content. For more control, modify the VCL configuration.
## Production Considerations
1. **Increase Varnish memory** for high-traffic sites
2. **Add monitoring** (Varnish stats, cache hit rates)
3. **Implement cache warming** for critical images
4. **Add rate limiting** for the image proxy
5. **Consider Varnish Plus** for enterprise features
## File Structure
```
varnish/
├── default.vcl # Varnish configuration
docker-compose.yml # Multi-container setup
api/
├── image.php # Image proxy with caching
└── purge_cache.php # Cache purging API
.htaccess # Apache caching rules
```
-148
View File
@@ -1,148 +0,0 @@
<?php
require_once '../include/init.php';
require_once '../include/auth.php';
// Set proper headers
header('Content-Type: application/json');
// Check if URL parameter is provided
if (!isset($_GET['url']) || empty($_GET['url'])) {
http_response_code(400);
echo json_encode(['error' => 'URL parameter is required']);
exit;
}
$imageUrl = $_GET['url'];
$width = isset($_GET['w']) ? intval($_GET['w']) : null;
$height = isset($_GET['h']) ? intval($_GET['h']) : null;
$quality = isset($_GET['q']) ? intval($_GET['q']) : 85;
// Validate URL
if (!filter_var($imageUrl, FILTER_VALIDATE_URL)) {
http_response_code(400);
echo json_encode(['error' => 'Invalid URL']);
exit;
}
// Allowed domains (add your CDN domains here)
$allowedDomains = [
'cdn.jsdelivr.net',
'favicon.hibbit.net',
'favicon.hibbit.cloud',
'localhost',
'127.0.0.1'
];
$parsedUrl = parse_url($imageUrl);
$domain = $parsedUrl['host'] ?? '';
if (!in_array($domain, $allowedDomains)) {
http_response_code(403);
echo json_encode(['error' => 'Domain not allowed']);
exit;
}
// Create cache directory
$cacheDir = '../data/images/cache/';
if (!is_dir($cacheDir)) {
mkdir($cacheDir, 0755, true);
}
// Generate cache key
$cacheKey = md5($imageUrl . $width . $height . $quality);
$cacheFile = $cacheDir . $cacheKey;
// Check if cached version exists and is fresh (1 day)
if (file_exists($cacheFile) && (time() - filemtime($cacheFile)) < 86400) {
// Serve cached file
$imageInfo = getimagesize($cacheFile);
if ($imageInfo) {
header('Content-Type: ' . $imageInfo['mime']);
header('Content-Length: ' . filesize($cacheFile));
header('Cache-Control: public, max-age=86400');
header('Expires: ' . gmdate('D, d M Y H:i:s', time() + 86400) . ' GMT');
header('ETag: "' . $cacheKey . '"');
readfile($cacheFile);
exit;
}
}
// Fetch the image
$context = stream_context_create([
'http' => [
'timeout' => 30,
'user_agent' => 'Bookmarkly Image Proxy/1.0',
'follow_location' => true,
'max_redirects' => 5
]
]);
$imageData = @file_get_contents($imageUrl, false, $context);
if ($imageData === false) {
http_response_code(404);
echo json_encode(['error' => 'Failed to fetch image']);
exit;
}
// Check if ImageMagick is available for processing
if (extension_loaded('imagick') && ($width || $height || $quality < 100)) {
try {
$imagick = new Imagick();
$imagick->readImageBlob($imageData);
// Resize if dimensions are specified
if ($width || $height) {
$imagick->resizeImage(
$width ?: 0,
$height ?: 0,
Imagick::FILTER_LANCZOS,
1
);
}
// Set quality for JPEG
if ($imagick->getImageFormat() === 'JPEG') {
$imagick->setImageCompressionQuality($quality);
}
$processedData = $imagick->getImageBlob();
$imagick->destroy();
// Save to cache
file_put_contents($cacheFile, $processedData);
// Set headers
$imageInfo = getimagesize($cacheFile);
header('Content-Type: ' . $imageInfo['mime']);
header('Content-Length: ' . strlen($processedData));
header('Cache-Control: public, max-age=86400');
header('Expires: ' . gmdate('D, d M Y H:i:s', time() + 86400) . ' GMT');
header('ETag: "' . $cacheKey . '"');
echo $processedData;
exit;
} catch (Exception $e) {
// Fallback to original image if processing fails
error_log('Image processing failed: ' . $e->getMessage());
}
}
// Serve original image and cache it
file_put_contents($cacheFile, $imageData);
$imageInfo = @getimagesize($cacheFile);
if ($imageInfo) {
header('Content-Type: ' . $imageInfo['mime']);
} else {
header('Content-Type: image/jpeg'); // fallback
}
header('Content-Length: ' . strlen($imageData));
header('Cache-Control: public, max-age=86400');
header('Expires: ' . gmdate('D, d M Y H:i:s', time() + 86400) . ' GMT');
header('ETag: "' . $cacheKey . '"');
echo $imageData;
?>
-104
View File
@@ -1,104 +0,0 @@
<?php
require_once '../include/init.php';
require_once '../include/auth.php';
// Only admins can purge cache
if (!isAdmin()) {
http_response_code(403);
echo json_encode(['success' => false, 'message' => 'Admin access required']);
exit;
}
header('Content-Type: application/json');
$purgeType = $_POST['type'] ?? $_GET['type'] ?? 'all';
$purgeUrl = $_POST['url'] ?? $_GET['url'] ?? '';
try {
switch ($purgeType) {
case 'all':
// Purge all cache
$cacheDir = '../data/images/cache/';
if (is_dir($cacheDir)) {
$files = glob($cacheDir . '*');
foreach ($files as $file) {
if (is_file($file)) {
unlink($file);
}
}
}
// Send Varnish purge request
$varnishHost = 'varnish';
$varnishPort = 80;
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "http://{$varnishHost}:{$varnishPort}/");
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PURGE');
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Purge-Regex: .*']);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
curl_exec($ch);
curl_close($ch);
echo json_encode(['success' => true, 'message' => 'All cache purged successfully']);
break;
case 'image':
if (empty($purgeUrl)) {
throw new Exception('URL parameter required for image purge');
}
// Purge specific image from cache
$cacheKey = md5($purgeUrl);
$cacheFile = '../data/images/cache/' . $cacheKey;
if (file_exists($cacheFile)) {
unlink($cacheFile);
}
// Send Varnish purge request for specific URL
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "http://varnish:80" . $purgeUrl);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PURGE');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
curl_exec($ch);
curl_close($ch);
echo json_encode(['success' => true, 'message' => 'Image cache purged successfully']);
break;
case 'images':
// Purge all image cache
$cacheDir = '../data/images/cache/';
if (is_dir($cacheDir)) {
$files = glob($cacheDir . '*');
foreach ($files as $file) {
if (is_file($file)) {
unlink($file);
}
}
}
// Send Varnish purge request for images
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "http://varnish:80/");
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PURGE');
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Purge-Regex: .*\.(jpg|jpeg|png|gif|webp|svg|ico)$']);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
curl_exec($ch);
curl_close($ch);
echo json_encode(['success' => true, 'message' => 'Image cache purged successfully']);
break;
default:
throw new Exception('Invalid purge type');
}
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
?>
+1 -1
View File
@@ -467,7 +467,7 @@ require_once 'include/header.php';
<li class="list-group-item d-flex justify-content-between align-items-center" data-id="<?php echo $bookmark['id']; ?>"> <li class="list-group-item d-flex justify-content-between align-items-center" data-id="<?php echo $bookmark['id']; ?>">
<span> <span>
<i class="bi bi-grip-vertical sort-handle" style="cursor: move;"></i> <i class="bi bi-grip-vertical sort-handle" style="cursor: move;"></i>
<img src="<?php echo $icon_src; ?>" class="ms-2 me-2" style="width: 16px; height: 16px;" onerror="this.onerror=null;this.src='include/images/default-icon.png';"> <img src="<?php echo $icon_src; ?>" class="ms-2 me-2" style="width: 16px; height: 16px;" loading="lazy" onerror="this.onerror=null;this.src='include/images/default-icon.png';">
<a href="<?php echo htmlspecialchars($bookmark['url']); ?>" <?php if($settings['open_new_tab'] ?? '1' === '1') echo 'target="_blank"'; ?>><?php echo htmlspecialchars($bookmark['title']); ?></a> <a href="<?php echo htmlspecialchars($bookmark['url']); ?>" <?php if($settings['open_new_tab'] ?? '1' === '1') echo 'target="_blank"'; ?>><?php echo htmlspecialchars($bookmark['title']); ?></a>
<?php if ($bookmark['is_favorite']): ?> <?php if ($bookmark['is_favorite']): ?>
<i class="bi bi-star-fill text-warning" title="Favorite"></i> <i class="bi bi-star-fill text-warning" title="Favorite"></i>
+11 -30
View File
@@ -1,36 +1,17 @@
version: '3.8'
services: services:
varnish: php:
image: varnish:7.3 build: .
container_name: bookmarkly-varnish container_name: bookmarkly2
restart: unless-stopped
ports: ports:
- "8080:80" - "8080:80"
- "6082:6082" # Varnish admin port
volumes: volumes:
- ./varnish/default.vcl:/etc/varnish/default.vcl:ro - .:/var/www/html:rw
environment: environment:
- VARNISH_SIZE=256M TZ: "Europe/Amsterdam"
command: ["varnishd", "-F", "-f", "/etc/varnish/default.vcl", "-a", "0.0.0.0:80", "-T", "0.0.0.0:6082", "-s", "malloc,256M"]
depends_on:
- app
restart: unless-stopped
networks:
- bookmarkly-network
app: # favicon-downloader:
build: . # ports:
container_name: bookmarkly-app # - 3000:3000
expose: # image: seadfeng/favicon-downloader
- "80" # restart: always
volumes:
- ./data:/var/www/html/data
environment:
- APACHE_DOCUMENT_ROOT=/var/www/html
restart: unless-stopped
networks:
- bookmarkly-network
networks:
bookmarkly-network:
driver: bridge
+17
View File
@@ -3,3 +3,20 @@ post_max_size = 100M
memory_limit = 512M memory_limit = 512M
max_execution_time = 300 max_execution_time = 300
extension=imagick.so extension=imagick.so
[opcache]
opcache.enable=1
opcache.memory_consumption=128
opcache.interned_strings_buffer=8
opcache.max_accelerated_files=4000
opcache.revalidate_freq=60
opcache.fast_shutdown=1
opcache.enable_cli=1
; Session configuration
session.cookie_lifetime = 86400
session.gc_maxlifetime = 86400
session.cookie_httponly = 1
session.use_strict_mode = 1
session.cookie_secure = 0
session.cookie_samesite = "Lax"
+7
View File
@@ -57,10 +57,17 @@ if ($theme === 'dark' || in_array('dark-background-active', $body_classes)) {
<meta name="robots" content="noindex, nofollow"> <meta name="robots" content="noindex, nofollow">
<title><?php echo htmlspecialchars($settings['site_title'] ?? 'Bookmarkly'); ?></title> <title><?php echo htmlspecialchars($settings['site_title'] ?? 'Bookmarkly'); ?></title>
<link rel="icon" href="<?php echo htmlspecialchars($settings['site_favicon'] ?? $settings['site_logo'] ?? 'include/images/favicon.ico'); ?>"> <link rel="icon" href="<?php echo htmlspecialchars($settings['site_favicon'] ?? $settings['site_logo'] ?? 'include/images/favicon.ico'); ?>">
<!-- Apple Touch Icons -->
<link rel="apple-touch-icon" sizes="180x180" href="<?php echo htmlspecialchars($settings['site_favicon'] ?? $settings['site_logo'] ?? 'include/images/favicon.ico'); ?>">
<link rel="apple-touch-icon" sizes="152x152" href="<?php echo htmlspecialchars($settings['site_favicon'] ?? $settings['site_logo'] ?? 'include/images/favicon.ico'); ?>">
<link rel="apple-touch-icon" sizes="120x120" href="<?php echo htmlspecialchars($settings['site_favicon'] ?? $settings['site_logo'] ?? 'include/images/favicon.ico'); ?>">
<link rel="preconnect" href="https://cdn.jsdelivr.net">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css" rel="stylesheet">
<link href="include/css/style.css" rel="stylesheet"> <link href="include/css/style.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/sortablejs@latest/Sortable.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/sortablejs@latest/Sortable.min.js"></script>
<script defer src="https://analytics.hibbit.cloud/script.js" data-website-id="0f6a81c8-d2f5-4849-a2d1-3eff2cb8819e"></script>
<style> <style>
:root { :root {
--bg-brightness: <?php echo intval($bg_brightness); ?>%; --bg-brightness: <?php echo intval($bg_brightness); ?>%;
+4
View File
@@ -6,6 +6,10 @@ ini_set('session.cookie_httponly', 1);
ini_set('session.use_strict_mode', 1); ini_set('session.use_strict_mode', 1);
ini_set('session.cookie_secure', isset($_SERVER['HTTPS']) ? 1 : 0); ini_set('session.cookie_secure', isset($_SERVER['HTTPS']) ? 1 : 0);
// Set default session cookie lifetime to 24 hours (86400 seconds)
ini_set('session.cookie_lifetime', 86400);
ini_set('session.gc_maxlifetime', 86400);
session_start(); session_start();
// The $pdo object is available from init.php which included this file. // The $pdo object is available from init.php which included this file.
+2 -2
View File
@@ -166,7 +166,7 @@ require_once 'include/header.php';
?> ?>
<div class="col-lg-2 col-md-3 col-sm-4 col-6 mb-3"> <div class="col-lg-2 col-md-3 col-sm-4 col-6 mb-3">
<a href="<?php echo htmlspecialchars($bookmark['url']); ?>" <?php echo $target_attr; ?> class="btn d-flex align-items-center text-start p-2 bookmark-link"> <a href="<?php echo htmlspecialchars($bookmark['url']); ?>" <?php echo $target_attr; ?> class="btn d-flex align-items-center text-start p-2 bookmark-link">
<img src="<?php echo $icon_src; ?>" alt="" class="bookmark-favorite-icon me-3" onerror="this.onerror=null;this.src='include/images/default-icon.png';"> <img src="<?php echo $icon_src; ?>" alt="" class="bookmark-favorite-icon me-3" loading="lazy" onerror="this.onerror=null;this.src='include/images/default-icon.png';">
<div> <div>
<span><?php echo htmlspecialchars($bookmark['title']); ?></span> <span><?php echo htmlspecialchars($bookmark['title']); ?></span>
<small class="d-block text-muted"><?php echo htmlspecialchars($display_url); ?></small> <small class="d-block text-muted"><?php echo htmlspecialchars($display_url); ?></small>
@@ -209,7 +209,7 @@ require_once 'include/header.php';
} }
?> ?>
<a href="<?php echo htmlspecialchars($bookmark['url']); ?>" <?php echo $target_attr; ?> class="list-group-item list-group-item-action d-flex align-items-center"> <a href="<?php echo htmlspecialchars($bookmark['url']); ?>" <?php echo $target_attr; ?> class="list-group-item list-group-item-action d-flex align-items-center">
<img src="<?php echo $icon_src; ?>" alt="" class="bookmark-icon me-2" onerror="this.onerror=null;this.src='include/images/default-icon.png';"> <img src="<?php echo $icon_src; ?>" alt="" class="bookmark-icon me-2" loading="lazy" onerror="this.onerror=null;this.src='include/images/default-icon.png';">
<span><?php echo htmlspecialchars($bookmark['title']); ?></span> <span><?php echo htmlspecialchars($bookmark['title']); ?></span>
</a> </a>
<?php endforeach; ?> <?php endforeach; ?>
+1 -1
View File
@@ -47,7 +47,7 @@ return [
'modal_category' => 'Category', 'modal_category' => 'Category',
'modal_select_category' => 'Select a category...', 'modal_select_category' => 'Select a category...',
'modal_icon_url' => 'Icon URL (optional)', 'modal_icon_url' => 'Icon URL (optional)',
'modal_icon_url_help' => 'Find icons at <a href="https://selfh.st/icons/" target="_blank">selfh.st/icons</a> and <a href="https://dashboardicons.com/icons" target="_blank">dashboardicons.com/icons</a>.', 'modal_icon_url_help' => 'Find icons at <a href="https://selfh.st/icons/" target="_blank">selfh.st</a> and <a href="https://dashboardicons.com/icons" target="_blank">dashboardicons.com/icons</a>.',
'modal_icon_upload' => 'Upload Icon (optional)', 'modal_icon_upload' => 'Upload Icon (optional)',
'modal_current_icon' => 'Current Icon', 'modal_current_icon' => 'Current Icon',
'modal_remove_icon_button' => 'Remove Custom Icon', 'modal_remove_icon_button' => 'Remove Custom Icon',
+5 -5
View File
@@ -66,13 +66,13 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$seconds = 14 * 24 * 60 * 60; // Default to 2 weeks $seconds = 14 * 24 * 60 * 60; // Default to 2 weeks
} }
// Set session cookie lifetime // Update session cookie with extended lifetime
ini_set('session.cookie_lifetime', $seconds);
ini_set('session.gc_maxlifetime', $seconds);
// Update session cookie with new lifetime
$params = session_get_cookie_params(); $params = session_get_cookie_params();
setcookie(session_name(), session_id(), time() + $seconds, $params['path'], $params['domain'], $params['secure'], $params['httponly']); setcookie(session_name(), session_id(), time() + $seconds, $params['path'], $params['domain'], $params['secure'], $params['httponly']);
} else {
// For normal login, set cookie to expire at end of browser session
$params = session_get_cookie_params();
setcookie(session_name(), session_id(), 0, $params['path'], $params['domain'], $params['secure'], $params['httponly']);
} }
header('Location: index.php'); header('Location: index.php');
+128 -47
View File
@@ -6,6 +6,86 @@ $GLOBALS['body_class'] = 'settings-page';
require_once 'include/auth.php'; require_once 'include/auth.php';
/**
* Resize an image while maintaining aspect ratio and transparency
* @param string $source_path Path to source image
* @param string $destination_path Path to save resized image
* @param int $max_width Maximum width
* @param int $max_height Maximum height
* @param string $extension File extension
* @return bool Success status
*/
function resizeImage($source_path, $destination_path, $max_width, $max_height, $extension) {
// Get original image dimensions
$image_info = getimagesize($source_path);
if ($image_info === false) {
return false;
}
$original_width = $image_info[0];
$original_height = $image_info[1];
// Calculate new dimensions maintaining aspect ratio
$ratio = min($max_width / $original_width, $max_height / $original_height);
$new_width = round($original_width * $ratio);
$new_height = round($original_height * $ratio);
// Create image resource based on file type
switch (strtolower($extension)) {
case 'jpg':
case 'jpeg':
$source_image = imagecreatefromjpeg($source_path);
break;
case 'png':
$source_image = imagecreatefrompng($source_path);
break;
case 'gif':
$source_image = imagecreatefromgif($source_path);
break;
default:
return false;
}
if ($source_image === false) {
return false;
}
// Create new image with transparency support
$new_image = imagecreatetruecolor($new_width, $new_height);
// Preserve transparency for PNG and GIF
if ($extension === 'png' || $extension === 'gif') {
imagealphablending($new_image, false);
imagesavealpha($new_image, true);
$transparent = imagecolorallocatealpha($new_image, 255, 255, 255, 127);
imagefilledrectangle($new_image, 0, 0, $new_width, $new_height, $transparent);
}
// Resize the image
imagecopyresampled($new_image, $source_image, 0, 0, 0, 0, $new_width, $new_height, $original_width, $original_height);
// Save the resized image
$result = false;
switch (strtolower($extension)) {
case 'jpg':
case 'jpeg':
$result = imagejpeg($new_image, $destination_path, 90);
break;
case 'png':
$result = imagepng($new_image, $destination_path, 9);
break;
case 'gif':
$result = imagegif($new_image, $destination_path);
break;
}
// Clean up memory
imagedestroy($source_image);
imagedestroy($new_image);
return $result;
}
// Flash messages for feedback // Flash messages for feedback
$message = ''; $message = '';
$message_type = ''; $message_type = '';
@@ -68,10 +148,44 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (in_array($favicon_ext, $allowed_favicon_ext)) { if (in_array($favicon_ext, $allowed_favicon_ext)) {
$favicon_filename = 'favicon.' . $favicon_ext; $favicon_filename = 'favicon.' . $favicon_ext;
$favicon_path = $uploadDir . $favicon_filename; $favicon_path = $uploadDir . $favicon_filename;
if (move_uploaded_file($_FILES['site_favicon']['tmp_name'], $favicon_path)) {
$upsert_stmt->execute(['site_favicon', $favicon_path . '?v=' . time()]); // 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 { } else {
$_SESSION['flash_message'] = ['text' => 'Failed to move uploaded favicon.', 'type' => 'danger']; // 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 { } else {
$_SESSION['flash_message'] = ['text' => 'Invalid file type for favicon.', 'type' => 'danger']; $_SESSION['flash_message'] = ['text' => 'Invalid file type for favicon.', 'type' => 'danger'];
@@ -277,7 +391,15 @@ require_once 'include/header.php';
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="site_favicon" class="form-label">Site Favicon</label> <label for="site_favicon" class="form-label">Site Favicon</label>
<input type="file" class="form-control" id="site_favicon" name="site_favicon"> <input type="file" class="form-control" id="site_favicon" name="site_favicon" accept="image/png,image/jpeg,image/gif,image/svg+xml,image/x-icon">
<div class="form-text">
<strong>Apple Touch Icon Requirements:</strong><br>
• Minimum size: 180x180 pixels<br>
• Maximum size: 256x256 pixels<br>
• Supported formats: PNG, JPG, GIF, SVG, ICO<br>
• Images larger than 256x256 will be automatically resized<br>
• Images smaller than 180x180 will be rejected
</div>
<?php if (!empty($settings['site_favicon'])): ?> <?php if (!empty($settings['site_favicon'])): ?>
<small class="form-text text-muted d-block mt-2">Current favicon: <img src="<?php echo htmlspecialchars($settings['site_favicon']); ?>" alt="Current Favicon" style="max-height: 16px; vertical-align: middle;"></small> <small class="form-text text-muted d-block mt-2">Current favicon: <img src="<?php echo htmlspecialchars($settings['site_favicon']); ?>" alt="Current Favicon" style="max-height: 16px; vertical-align: middle;"></small>
<?php endif; ?> <?php endif; ?>
@@ -487,12 +609,11 @@ require_once 'include/header.php';
</div> </div>
</div> </div>
<!-- System Tab --> <!-- System Tab -->
<?php // Temporarily disabled admin check for testing: if (isAdmin()): ?> <?php if (isAdmin()): ?>
<div class="tab-pane fade" id="v-pills-system" role="tabpanel"> <div class="tab-pane fade" id="v-pills-system" role="tabpanel">
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<h4>System Maintenance</h4> <h4>System Maintenance</h4>
<small class="text-muted">Debug: Admin role detected</small>
</div> </div>
<div class="card-body"> <div class="card-body">
<h5>Update Database Schema</h5> <h5>Update Database Schema</h5>
@@ -513,17 +634,10 @@ require_once 'include/header.php';
<p>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.</p> <p>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.</p>
<button class="btn btn-info" id="fix-public-tabs-button">Fix Public Tabs</button> <button class="btn btn-info" id="fix-public-tabs-button">Fix Public Tabs</button>
<div id="fix-public-tabs-result" class="mt-3"></div> <div id="fix-public-tabs-result" class="mt-3"></div>
<hr class="my-4">
<h5>Clear Varnish Cache</h5>
<p>This will purge all cached content from Varnish Cache. This is useful when you've made changes to images or static content and want to ensure users see the latest version.</p>
<button class="btn btn-warning" id="clear-cache-button">Clear Varnish Cache</button>
<div id="clear-cache-result" class="mt-3"></div>
</div> </div>
</div> </div>
</div> </div>
<?php // endif; ?> <?php endif; ?>
</div> </div>
</div> </div>
</div> </div>
@@ -643,39 +757,6 @@ document.addEventListener('DOMContentLoaded', function() {
}); });
} }
// --- Handle Clear Varnish Cache ---
const clearCacheBtn = document.getElementById('clear-cache-button');
const clearCacheResult = document.getElementById('clear-cache-result');
if (clearCacheBtn) {
clearCacheBtn.addEventListener('click', async function() {
clearCacheBtn.disabled = true;
clearCacheResult.innerHTML = `<div class="spinner-border spinner-border-sm" role="status"><span class="visually-hidden">Loading...</span></div> Clearing Varnish cache...`;
try {
const response = await fetch('api/purge_cache.php', {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
});
const data = await response.json();
if (response.ok && data.success) {
clearCacheResult.innerHTML = `<div class="alert alert-success"><strong>${data.message}</strong></div>`;
} else {
throw new Error(data.message || 'An unknown error occurred.');
}
} catch (error) {
clearCacheResult.innerHTML = `<div class="alert alert-danger"><strong>Error:</strong> ${error.message}</div>`;
} finally {
clearCacheBtn.disabled = false;
}
});
}
// --- New: Handle Backup Import to Tab --- // --- New: Handle Backup Import to Tab ---
const importForm = document.getElementById('import-backup-form'); const importForm = document.getElementById('import-backup-form');
if (importForm) { if (importForm) {
-148
View File
@@ -1,148 +0,0 @@
vcl 4.1;
# Backend definition - points to our PHP/Apache container
backend default {
.host = "app";
.port = "80";
.connect_timeout = 60s;
.first_byte_timeout = 60s;
.between_bytes_timeout = 60s;
}
# Called at the beginning of a request
sub vcl_recv {
# Remove port from Host header for proper backend routing
if (req.http.Host) {
set req.http.Host = regsub(req.http.Host, ":[0-9]+", "");
}
# Handle image requests
if (req.url ~ "\.(jpg|jpeg|png|gif|webp|svg|ico)$") {
# Cache images aggressively
set req.http.Cache-Control = "public, max-age=2592000";
return (hash);
}
# Handle API image proxy requests
if (req.url ~ "^/api/image\.php") {
# Cache external images through our proxy
return (hash);
}
# Handle CSS and JS files
if (req.url ~ "\.(css|js)$") {
return (hash);
}
# Don't cache POST, PUT, DELETE requests
if (req.method != "GET" && req.method != "HEAD") {
return (pass);
}
# Don't cache requests with cookies (except for images)
if (req.http.Cookie && req.url !~ "\.(jpg|jpeg|png|gif|webp|svg|ico|css|js)$") {
return (pass);
}
# Remove tracking parameters
if (req.url ~ "\?") {
set req.url = regsub(req.url, "\?.*", "");
}
return (hash);
}
# Called after a cache lookup
sub vcl_hit {
# Add cache hit header for debugging
set req.http.X-Cache = "HIT";
return (deliver);
}
# Called after a cache miss
sub vcl_miss {
set req.http.X-Cache = "MISS";
return (fetch);
}
# Called before sending response to client
sub vcl_deliver {
# Add cache status header
if (req.http.X-Cache) {
set resp.http.X-Cache = req.http.X-Cache;
}
# Add CORS headers for images
if (req.url ~ "\.(jpg|jpeg|png|gif|webp|svg|ico)$") {
set resp.http.Access-Control-Allow-Origin = "*";
}
# Remove server identification
unset resp.http.Server;
unset resp.http.X-Powered-By;
unset resp.http.X-Varnish;
return (deliver);
}
# Called after fetching from backend
sub vcl_backend_response {
# Cache images for 30 days
if (bereq.url ~ "\.(jpg|jpeg|png|gif|webp|svg|ico)$") {
set beresp.ttl = 30d;
set beresp.http.Cache-Control = "public, max-age=2592000";
set beresp.http.Expires = beresp.http.Date + 30d;
# Don't cache if backend says not to
if (beresp.http.Cache-Control ~ "no-cache") {
set beresp.ttl = 0s;
}
}
# Cache CSS and JS for 7 days
if (bereq.url ~ "\.(css|js)$") {
set beresp.ttl = 7d;
set beresp.http.Cache-Control = "public, max-age=604800";
}
# Cache API image proxy for 1 day
if (bereq.url ~ "^/api/image\.php") {
set beresp.ttl = 1d;
set beresp.http.Cache-Control = "public, max-age=86400";
}
# Don't cache if there's an error
if (beresp.status >= 400) {
set beresp.ttl = 0s;
set beresp.uncacheable = true;
}
# Enable compression
if (beresp.http.Content-Type ~ "text|application|image/svg") {
set beresp.do_gzip = true;
}
return (deliver);
}
# Called when there's an error
sub vcl_backend_error {
set beresp.http.Content-Type = "text/html; charset=utf-8";
set beresp.status = 503;
synthetic( {"<!DOCTYPE html>
<html>
<head>
<title>Service Unavailable</title>
<style>
body { font-family: Arial, sans-serif; text-align: center; padding: 50px; }
h1 { color: #666; }
</style>
</head>
<body>
<h1>Service Temporarily Unavailable</h1>
<p>Please try again in a few moments.</p>
</body>
</html>"} );
return (deliver);
}