Compare commits
13 Commits
renovate/c
...
rewrite-ph
| Author | SHA1 | Date | |
|---|---|---|---|
|
0a25479e70
|
|||
|
85a5ce8221
|
|||
|
ffc7877b0e
|
|||
|
2e9cb4d6af
|
|||
|
d099a33191
|
|||
|
4d9fec13dd
|
|||
|
286a91b125
|
|||
|
738c72ebf2
|
|||
|
97872ef9fb
|
|||
|
22b3fe9d0b
|
|||
|
3a2926675a
|
|||
|
74c8eacbf2
|
|||
|
9ab4fcbe81
|
@@ -25,7 +25,7 @@ jobs:
|
||||
echo "REPO_NAME=$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV
|
||||
|
||||
- name: setup uv
|
||||
uses: astral-sh/setup-uv@v5
|
||||
uses: astral-sh/setup-uv@v7
|
||||
- name: Install Python
|
||||
run: uv python install
|
||||
with:
|
||||
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
fetch-tags: true # Fetch all tags (refs/tags)
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v5
|
||||
uses: astral-sh/setup-uv@v7
|
||||
- name: Set up Python
|
||||
run: uv python install
|
||||
with:
|
||||
|
||||
97
API_SERVICE.md
Normal file
97
API_SERVICE.md
Normal file
@@ -0,0 +1,97 @@
|
||||
# Signature Validation API Service
|
||||
|
||||
This is a lightweight Python service that provides signature validation for the PHP application.
|
||||
|
||||
## Why a separate service?
|
||||
|
||||
The `bibapi` library is Python-only and provides access to your library catalog. Rather than rewriting this in PHP, we keep a small Python service running just for signature validation.
|
||||
|
||||
## Running the Service
|
||||
|
||||
### Option 1: Direct Python
|
||||
```bash
|
||||
python api_service.py
|
||||
```
|
||||
|
||||
### Option 2: With uvicorn
|
||||
```bash
|
||||
uvicorn api_service:app --host 0.0.0.0 --port 8001
|
||||
```
|
||||
|
||||
### Option 3: Docker (if you can run containers internally)
|
||||
```dockerfile
|
||||
FROM python:3.13-slim
|
||||
WORKDIR /app
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
COPY api_service.py .
|
||||
EXPOSE 8001
|
||||
CMD ["uvicorn", "api_service:app", "--host", "0.0.0.0", "--port", "8001"]
|
||||
```
|
||||
|
||||
```bash
|
||||
docker build -t signature-api .
|
||||
docker run -d -p 8001:8001 signature-api
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Set the API endpoint in your PHP config. Update `php/config.php`:
|
||||
|
||||
```php
|
||||
// Signature validation API endpoint (optional)
|
||||
define('SIGNATURE_API_URL', getenv('SIGNATURE_API_URL') ?: 'http://localhost:8001');
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
# Health check
|
||||
curl http://localhost:8001/health
|
||||
|
||||
# Validate a signature
|
||||
curl "http://localhost:8001/api/validate-signature?signature=ABC123"
|
||||
```
|
||||
|
||||
## Production Deployment
|
||||
|
||||
1. **Same server**: Run on a different port (8001) alongside your PHP application
|
||||
2. **Separate server**: Run on internal network, update `SIGNATURE_API_URL` in PHP config
|
||||
3. **Systemd service** (Linux):
|
||||
|
||||
Create `/etc/systemd/system/signature-api.service`:
|
||||
```ini
|
||||
[Unit]
|
||||
Description=Signature Validation API
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=www-data
|
||||
WorkingDirectory=/var/www/signature-api
|
||||
Environment="PATH=/var/www/signature-api/.venv/bin"
|
||||
ExecStart=/var/www/signature-api/.venv/bin/uvicorn api_service:app --host 0.0.0.0 --port 8001
|
||||
Restart=always
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
Then:
|
||||
```bash
|
||||
sudo systemctl enable signature-api
|
||||
sudo systemctl start signature-api
|
||||
```
|
||||
|
||||
## Security
|
||||
|
||||
- In production, update CORS `allow_origins` to only your PHP server domain
|
||||
- Consider adding API key authentication if exposed to public network
|
||||
- Run behind reverse proxy (nginx/Apache) with SSL
|
||||
|
||||
## Notes
|
||||
|
||||
- The service is stateless and lightweight
|
||||
- No data persistence required
|
||||
- Can be scaled horizontally if needed
|
||||
- Falls back gracefully if unavailable (ELSA form fields just won't have validation hints)
|
||||
73
api_service.py
Normal file
73
api_service.py
Normal file
@@ -0,0 +1,73 @@
|
||||
"""
|
||||
Lightweight Python API service for signature validation
|
||||
This can run independently to support the PHP application
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
|
||||
from bibapi import catalogue
|
||||
from fastapi import FastAPI, Query
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
app = FastAPI(title="Signature Validation API")
|
||||
|
||||
# Allow PHP application to call this API
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"], # In production, restrict to your PHP server domain
|
||||
allow_credentials=True,
|
||||
allow_methods=["GET"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# Initialize catalogue for signature validation
|
||||
cat = catalogue.Catalogue()
|
||||
|
||||
|
||||
@app.get("/api/validate-signature")
|
||||
async def validate_signature(signature: str = Query(...)):
|
||||
"""Validate a book signature and return total pages"""
|
||||
try:
|
||||
book_result = cat.get_book_with_data(signature)
|
||||
if book_result and hasattr(book_result, "pages") and book_result.pages:
|
||||
# Try to extract numeric page count
|
||||
pages_str = str(book_result.pages)
|
||||
# Extract first number from pages string (e.g., "245 S." -> 245)
|
||||
match = re.search(r"(\d+)", pages_str)
|
||||
if match:
|
||||
total_pages = int(match.group(1))
|
||||
return JSONResponse(
|
||||
{"valid": True, "total_pages": total_pages, "signature": signature}
|
||||
)
|
||||
|
||||
return JSONResponse(
|
||||
{
|
||||
"valid": False,
|
||||
"error": "Signatur nicht gefunden oder keine Seitenzahl verfügbar",
|
||||
"signature": signature,
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
return JSONResponse(
|
||||
{
|
||||
"valid": False,
|
||||
"error": f"Fehler bei der Validierung: {str(e)}",
|
||||
"signature": signature,
|
||||
},
|
||||
status_code=500,
|
||||
)
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
"""Health check endpoint"""
|
||||
return {"status": "ok", "service": "signature-validation"}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
port = int(os.getenv("API_PORT", "8001"))
|
||||
uvicorn.run(app, host="0.0.0.0", port=port)
|
||||
25
php/.htaccess
Normal file
25
php/.htaccess
Normal file
@@ -0,0 +1,25 @@
|
||||
# Prevent directory browsing
|
||||
Options -Indexes
|
||||
|
||||
# Enable rewrite engine (optional, for clean URLs)
|
||||
RewriteEngine On
|
||||
|
||||
# Redirect /semesterapparat to semesterapparat.php
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteRule ^semesterapparat$ semesterapparat.php [L]
|
||||
|
||||
# Redirect /elsa to elsa.php
|
||||
RewriteRule ^elsa$ elsa.php [L]
|
||||
|
||||
# Protect config files
|
||||
<FilesMatch "^(config|functions)\.php$">
|
||||
Order allow,deny
|
||||
Deny from all
|
||||
</FilesMatch>
|
||||
|
||||
# Set default charset
|
||||
AddDefaultCharset UTF-8
|
||||
|
||||
# Error pages (customize as needed)
|
||||
ErrorDocument 404 /index.php
|
||||
100
php/OPENSSL_SETUP.md
Normal file
100
php/OPENSSL_SETUP.md
Normal file
@@ -0,0 +1,100 @@
|
||||
# Enabling OpenSSL Extension in PHP (Windows)
|
||||
|
||||
## Problem
|
||||
Your PHP installation doesn't have the OpenSSL extension enabled, which is required for secure SMTP connections (TLS/SSL).
|
||||
|
||||
Current error: `Unable to find the socket transport "ssl"` or `TLS/SSL transports not available`
|
||||
|
||||
## Solution
|
||||
|
||||
### Option 1: Enable OpenSSL in php.ini (Recommended)
|
||||
|
||||
1. **Find your php.ini file:**
|
||||
```powershell
|
||||
php --ini
|
||||
```
|
||||
|
||||
If it shows "Loaded Configuration File: (none)", you need to create one:
|
||||
```powershell
|
||||
# Find PHP installation directory
|
||||
where.exe php
|
||||
|
||||
# Copy the example php.ini
|
||||
cd C:\path\to\php
|
||||
copy php.ini-development php.ini
|
||||
```
|
||||
|
||||
2. **Edit php.ini** and find this line:
|
||||
```ini
|
||||
;extension=openssl
|
||||
```
|
||||
|
||||
Remove the semicolon to uncomment it:
|
||||
```ini
|
||||
extension=openssl
|
||||
```
|
||||
|
||||
3. **Restart your PHP server** (or command line if using built-in server)
|
||||
|
||||
4. **Verify OpenSSL is enabled:**
|
||||
```powershell
|
||||
php -m | Select-String -Pattern "openssl"
|
||||
```
|
||||
|
||||
Should output: `openssl`
|
||||
|
||||
### Option 2: Enable OpenSSL temporarily via command line
|
||||
|
||||
You can enable OpenSSL for a single command:
|
||||
|
||||
```powershell
|
||||
php -d extension=openssl test_email.php
|
||||
```
|
||||
|
||||
Or for the built-in server:
|
||||
|
||||
```powershell
|
||||
php -d extension=openssl -S localhost:8000
|
||||
```
|
||||
|
||||
### Option 3: Install Composer and PHPMailer
|
||||
|
||||
If you can't modify php.ini, install PHPMailer which may handle SMTP better:
|
||||
|
||||
```powershell
|
||||
# Install Composer first (https://getcomposer.org/)
|
||||
# Then in the php/ directory:
|
||||
composer require phpmailer/phpmailer
|
||||
```
|
||||
|
||||
The application will automatically detect and use PHPMailer if available.
|
||||
|
||||
## Verifying the Fix
|
||||
|
||||
After enabling OpenSSL, run the test:
|
||||
|
||||
```powershell
|
||||
cd C:\Users\aky547\GitHub\SemapForm\php
|
||||
php test_email.php
|
||||
```
|
||||
|
||||
You should see:
|
||||
- `Available transports: tcp, udp, ssl, tls` (or similar)
|
||||
- `TLS encryption enabled successfully`
|
||||
- `Email sent successfully via SMTP`
|
||||
|
||||
## Alternative: Use a Different SMTP Port
|
||||
|
||||
If you absolutely cannot enable OpenSSL, you could try:
|
||||
|
||||
1. Contact your email administrator to see if there's an unencrypted SMTP port (not recommended for security)
|
||||
2. Use a different email solution (e.g., send via a web service API instead of SMTP)
|
||||
|
||||
## Production Deployment
|
||||
|
||||
On a production server (Linux/shared hosting):
|
||||
|
||||
- OpenSSL is usually enabled by default
|
||||
- Check with `php -m | grep openssl`
|
||||
- If not available, contact your hosting provider
|
||||
- Most shared hosting environments have it pre-configured
|
||||
203
php/README.md
Normal file
203
php/README.md
Normal file
@@ -0,0 +1,203 @@
|
||||
# PHP Deployment Guide for SemapForm
|
||||
|
||||
## Requirements
|
||||
|
||||
- PHP 7.4 or higher (PHP 8+ recommended)
|
||||
- PHP extensions:
|
||||
- `dom` (for XML generation)
|
||||
- `mbstring` (for string handling)
|
||||
- `openssl` (for SMTP if using SSL/TLS)
|
||||
- Web server (Apache, Nginx, or any PHP-capable server)
|
||||
- Optional: PHPMailer library for advanced SMTP support
|
||||
|
||||
## Installation Steps
|
||||
|
||||
1. **Upload files to your PHP server:**
|
||||
```
|
||||
php/
|
||||
├── index.php
|
||||
├── semesterapparat.php
|
||||
├── submit_semesterapparat.php
|
||||
├── config.php
|
||||
├── functions.php
|
||||
├── .htaccess (if using Apache)
|
||||
└── static/
|
||||
└── styles.css
|
||||
```
|
||||
|
||||
2. **Configure email settings:**
|
||||
|
||||
Edit `config.php` or set environment variables:
|
||||
- `MAIL_ENABLED`: Set to `true` to enable email sending
|
||||
- `SMTP_HOST`: Your SMTP server (e.g., smtp.ph-freiburg.de)
|
||||
- `SMTP_PORT`: SMTP port (465 for SSL, 587 for TLS, 25 for plain)
|
||||
- `SMTP_USERNAME`: Your SMTP username
|
||||
- `SMTP_PASSWORD`: Your SMTP password
|
||||
- `MAIL_FROM`: Sender email address
|
||||
- `MAIL_TO`: Recipient email address
|
||||
|
||||
3. **Set permissions:**
|
||||
```bash
|
||||
chmod 755 *.php
|
||||
chmod 644 config.php
|
||||
chmod 644 static/styles.css
|
||||
```
|
||||
|
||||
4. **Test the installation:**
|
||||
- Navigate to your server URL (e.g., `https://yourserver.com/php/`)
|
||||
- Try submitting a test form
|
||||
- Check email delivery or server logs
|
||||
|
||||
## Email Configuration Options
|
||||
|
||||
### Option 1: PHP's built-in mail() function
|
||||
- Simplest setup
|
||||
- Requires server to have mail transfer agent (MTA) configured
|
||||
- No additional configuration needed in PHP
|
||||
|
||||
### Option 2: SMTP with PHP's native functions
|
||||
- Configure SMTP settings in `config.php`
|
||||
- Works with basic authentication
|
||||
- Current implementation in `functions.php`
|
||||
|
||||
### Option 3: PHPMailer (Recommended for production)
|
||||
- Install PHPMailer via Composer:
|
||||
```bash
|
||||
composer require phpmailer/phpmailer
|
||||
```
|
||||
- Better error handling and SMTP support
|
||||
- Already integrated in `functions.php` (will auto-detect if available)
|
||||
|
||||
## Server-Specific Notes
|
||||
|
||||
### Apache
|
||||
- The `.htaccess` file is included for URL rewriting and security
|
||||
- Ensure `mod_rewrite` is enabled
|
||||
|
||||
### Nginx
|
||||
- Add this to your nginx.conf:
|
||||
```nginx
|
||||
location /php {
|
||||
index index.php;
|
||||
try_files $uri $uri/ /index.php?$query_string;
|
||||
|
||||
location ~ \.php$ {
|
||||
fastcgi_pass unix:/var/run/php/php-fpm.sock;
|
||||
fastcgi_index index.php;
|
||||
include fastcgi_params;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Shared Hosting
|
||||
- Most shared hosts have PHP and mail() pre-configured
|
||||
- Upload files via FTP/SFTP
|
||||
- Set environment variables through hosting control panel
|
||||
- Test mail() function first before adding SMTP
|
||||
|
||||
## Email Logging
|
||||
|
||||
The application now includes comprehensive email logging:
|
||||
|
||||
- **Log Location**: `php/mail.log`
|
||||
- **Log Format**: `[YYYY-MM-DD HH:MM:SS] [LEVEL] Message`
|
||||
- **Configuration**:
|
||||
- Set `LOG_ENABLED = true` in `config.php` to enable logging
|
||||
- Logs are also written to PHP error_log with `[MAIL]` prefix
|
||||
|
||||
### What Gets Logged
|
||||
|
||||
- Email send attempts with from/to/subject
|
||||
- Email content length
|
||||
- SMTP connection details (host, port, encryption)
|
||||
- PHPMailer initialization and configuration steps
|
||||
- Success/failure status for each send attempt
|
||||
- Detailed error messages for failed sends
|
||||
- When mail is disabled (includes full XML content)
|
||||
|
||||
### Viewing Logs
|
||||
|
||||
```bash
|
||||
# View recent log entries
|
||||
tail -f php/mail.log
|
||||
|
||||
# View last 50 lines
|
||||
tail -n 50 php/mail.log
|
||||
|
||||
# Search for errors
|
||||
grep ERROR php/mail.log
|
||||
|
||||
# Search for successful sends
|
||||
grep SUCCESS php/mail.log
|
||||
```
|
||||
|
||||
### Log File Management
|
||||
|
||||
The log file is automatically created when needed. To rotate logs:
|
||||
|
||||
```bash
|
||||
# Manual rotation
|
||||
mv php/mail.log php/mail.log.$(date +%Y%m%d)
|
||||
touch php/mail.log
|
||||
chmod 666 php/mail.log
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Emails not sending
|
||||
1. Check `MAIL_ENABLED` is set to `true`
|
||||
2. **Check `php/mail.log`** for detailed error messages
|
||||
3. Verify SMTP credentials are correct
|
||||
4. Check server error logs: `tail -f /var/log/apache2/error.log`
|
||||
5. Test with `MAIL_ENABLED=false` to see XML output in logs
|
||||
|
||||
### Form validation errors
|
||||
- Ensure all required fields have values
|
||||
- Check email format validation
|
||||
- Verify POST data is being received
|
||||
|
||||
### Permission errors
|
||||
- Ensure PHP has read access to all files
|
||||
- Check web server user/group permissions
|
||||
|
||||
## Security Recommendations
|
||||
|
||||
1. **Move config.php outside web root** if possible
|
||||
2. **Use environment variables** for sensitive data
|
||||
3. **Enable HTTPS** for form submissions
|
||||
4. **Sanitize all inputs** (already implemented in `functions.php`)
|
||||
5. **Set production error reporting** in `config.php`:
|
||||
```php
|
||||
error_reporting(0);
|
||||
ini_set('display_errors', 0);
|
||||
```
|
||||
6. **Regular updates**: Keep PHP and server software updated
|
||||
|
||||
## Migrating from Docker/Python
|
||||
|
||||
The PHP version maintains feature parity with the Python/FastAPI version:
|
||||
- ✅ Same form fields and validation
|
||||
- ✅ XML generation with identical structure
|
||||
- ✅ Email sending with SMTP support
|
||||
- ✅ Same CSS and frontend behavior
|
||||
- ✅ Theme toggle functionality
|
||||
- ✅ Multi-book/media support
|
||||
- ✅ Optional fields (title, signature, Dauerapparat)
|
||||
|
||||
## Environment Variables
|
||||
|
||||
You can set these as server environment variables instead of editing `config.php`:
|
||||
|
||||
```bash
|
||||
export MAIL_ENABLED=true
|
||||
export SMTP_HOST=smtp.ph-freiburg.de
|
||||
export SMTP_PORT=465
|
||||
export MAIL_USERNAME=your_username
|
||||
export MAIL_PASSWORD=your_password
|
||||
export MAIL_FROM=alexander.kirchner@ph-freiburg.de
|
||||
export MAIL_TO=semesterapparate@ph-freiburg.de
|
||||
```
|
||||
|
||||
## Support
|
||||
|
||||
For issues specific to your hosting environment, consult your hosting provider's PHP documentation.
|
||||
35
php/config.php
Normal file
35
php/config.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
/**
|
||||
* Configuration file for SemapForm PHP Application
|
||||
*
|
||||
* Copy this file and adjust values for your environment.
|
||||
* For production, consider loading from environment variables or a .env file
|
||||
*/
|
||||
|
||||
// Error reporting (set to 0 in production)
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 1);
|
||||
|
||||
// Email configuration
|
||||
define('MAIL_ENABLED', true);//getenv('MAIL_ENABLED') !== false ? filter_var(getenv('MAIL_ENABLED'), FILTER_VALIDATE_BOOLEAN) : true);
|
||||
define('SMTP_HOST', 'smtp.ph-freiburg.de');//getenv('SMTP_HOST') ?: 'smtp.ph-freiburg.de');
|
||||
define('SMTP_PORT', 587);//getenv('SMTP_PORT') ?: 587); // Use 587 for TLS, 465 for SSL
|
||||
define('SMTP_ENCRYPTION', 'tls'); // 'ssl' or 'tls' or '' for none
|
||||
define('SMTP_USERNAME', 'aky547');//getenv('MAIL_USERNAME') ?: 'aky547');
|
||||
define('SMTP_PASSWORD', 'CMcDna3qPh2*n');//getenv('MAIL_PASSWORD') ?: 'CMcDna3qPh2*n');
|
||||
define('MAIL_FROM', 'alexander.kirchner@ph-freiburg.de');//getenv('MAIL_FROM') ?: 'alexander.kirchner@ph-freiburg.de');
|
||||
define('MAIL_TO', 'kirchneralexander020@gmail.com');//getenv('MAIL_TO') ?: 'kirchneralexander@proton.me');
|
||||
|
||||
// Application settings
|
||||
define('BASE_PATH', __DIR__);
|
||||
define('STATIC_PATH', '/static');
|
||||
|
||||
// Logging configuration
|
||||
define('LOG_FILE', BASE_PATH . '/mail.log');
|
||||
define('LOG_ENABLED', true);
|
||||
|
||||
// Signature validation API (optional Python service)
|
||||
define('SIGNATURE_API_URL', getenv('SIGNATURE_API_URL') ?: 'http://localhost:8001');
|
||||
|
||||
// Email regex pattern
|
||||
define('EMAIL_REGEX', '/^[^@\s]+@[^@\s]+\.[^@\s]+$/');
|
||||
523
php/elsa.php
Normal file
523
php/elsa.php
Normal file
@@ -0,0 +1,523 @@
|
||||
<?php require_once 'config.php'; ?>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>ELSA - Elektronischer Semesterapparat</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="stylesheet" href="<?php echo STATIC_PATH; ?>/styles.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@7.4.47/css/materialdesignicons.min.css">
|
||||
</head>
|
||||
<body>
|
||||
<header class="site-header">
|
||||
<div class="container header-inner">
|
||||
<div class="header-left">
|
||||
<img class="logo" src="https://www.ph-freiburg.de/_assets/cc3dd7db45300ecc1f3aeed85bda5532/Images/logo-big-blue.svg" alt="PH Freiburg Logo" />
|
||||
<div class="brand">
|
||||
<div class="brand-title">Bibliothek der Pädagogischen Hochschule Freiburg</div>
|
||||
<div class="brand-sub">Hochschulbibliothek · ELSA</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" id="theme-toggle" class="theme-toggle" aria-label="Switch to dark mode" title="Switch to dark mode">
|
||||
<span class="mdi mdi-theme-light-dark" aria-hidden="true"></span>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="container">
|
||||
<section class="card">
|
||||
<h1>ELSA - Elektronischer Semesterapparat</h1>
|
||||
|
||||
<div class="legal-notice">
|
||||
<h3><span class="mdi mdi-information"></span> Rechtlicher Hinweis</h3>
|
||||
<p>
|
||||
Das Urheberrecht gestattet gemäß § 60a UrhG zur Veranschaulichung des Unterrichts die Bereitstellung elektronischer Kopien bis max. 15% von veröffentlichten Werken, von einzelnen Aufsätzen aus Fachzeitschriften und von Werken geringen Umfangs (max. 25 S. Gesamtumfang) für einen genau abgegrenzten Benutzerkreis wie den Teilnehmenden einer Lehrveranstaltung, die sich auf ILIAS angemeldet haben.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form method="post" action="submit_elsa.php" class="request-form" data-api-url="<?php echo SIGNATURE_API_URL; ?>">
|
||||
<h2>Allgemeine Informationen</h2>
|
||||
<div class="grid-form">
|
||||
<div class="form-field">
|
||||
<label for="name">Name</label>
|
||||
<input type="text" name="name" id="name" required>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label for="lastname">Nachname</label>
|
||||
<input type="text" name="lastname" id="lastname" required>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label for="title">Titel (optional)</label>
|
||||
<input type="text" name="title" id="title">
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label for="mail">Email</label>
|
||||
<input type="email" name="mail" id="mail" required>
|
||||
</div>
|
||||
|
||||
<div class="form-field">
|
||||
<label for="subject">Fach</label>
|
||||
<select name="subject" id="subject" required>
|
||||
<option value="">-- Auswählen --</option>
|
||||
<option>Biologie</option>
|
||||
<option>Chemie</option>
|
||||
<option>Deutsch</option>
|
||||
<option>Englisch</option>
|
||||
<option>Erziehungswirtschaft</option>
|
||||
<option>Französisch</option>
|
||||
<option>Geographie</option>
|
||||
<option>Geschichte</option>
|
||||
<option>Gesundheitspädagogik</option>
|
||||
<option>Haushalt / Textil</option>
|
||||
<option>Kunst</option>
|
||||
<option>Mathematik / Informatik</option>
|
||||
<option>Medien in der Bildung</option>
|
||||
<option>Musik</option>
|
||||
<option>Philosophie</option>
|
||||
<option>Physik</option>
|
||||
<option>Politikwissenschaft</option>
|
||||
<option>Prorektorat Lehre und Studium</option>
|
||||
<option>Psychologie</option>
|
||||
<option>Soziologie</option>
|
||||
<option>Sport</option>
|
||||
<option>Technik</option>
|
||||
<option>Theologie</option>
|
||||
<option>Wirtschaftslehre</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label for="classname">Veranstaltungsname</label>
|
||||
<input type="text" name="classname" id="classname" required>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label for="usage_date_from">Nutzungszeitraum von</label>
|
||||
<input type="date" name="usage_date_from" id="usage_date_from" required>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label for="usage_date_to">Nutzungszeitraum bis</label>
|
||||
<input type="date" name="usage_date_to" id="usage_date_to" required>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label for="availability_date">Bereitstellungsdatum</label>
|
||||
<input type="date" name="availability_date" id="availability_date" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Medien</h2>
|
||||
<div class="media-controls">
|
||||
<button type="button" id="btn-monografie" class="btn btn-secondary" onclick="addMediaType('monografie')" title="Monografie Sektion hinzufügen">
|
||||
<span class="mdi mdi-book"></span> + Monografie
|
||||
</button>
|
||||
<button type="button" id="btn-zeitschriftenartikel" class="btn btn-secondary" onclick="addMediaType('zeitschriftenartikel')" title="Zeitschriftenartikel Sektion hinzufügen">
|
||||
<span class="mdi mdi-newspaper"></span> + Zeitschriftenartikel
|
||||
</button>
|
||||
<button type="button" id="btn-herausgeberwerk" class="btn btn-secondary" onclick="addMediaType('herausgeberwerk')" title="Herausgeberwerk Sektion hinzufügen">
|
||||
<span class="mdi mdi-book-multiple"></span> + Herausgeberwerk
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="media-sections"></div>
|
||||
|
||||
<div class="form-field" style="margin-top: 20px;">
|
||||
<label for="message">Nachricht (optional)</label>
|
||||
<textarea name="message" id="message" rows="4" placeholder="Zusätzliche Anmerkungen oder Hinweise..."></textarea>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button type="submit" class="btn btn-primary">Absenden</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<p class="footer-note">
|
||||
Hinweis: Weitere Informationen zu den Elektronischen Semesterapparaten finden Sie auf den Seiten der Hochschulbibliothek der PH Freiburg.
|
||||
<br>
|
||||
<a href="https://www.ph-freiburg.de/bibliothek.html" target="_blank" rel="noopener noreferrer">Zur Bibliothek</a>
|
||||
|
|
||||
<a href="https://ilias.ph-freiburg.de/goto.php/cat/141" target="_blank" rel="noopener noreferrer">Zu den Elektronischen Semesterapparaten</a>
|
||||
</p>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
let mediaCounter = {
|
||||
monografie: 0,
|
||||
zeitschriftenartikel: 0,
|
||||
herausgeberwerk: 0
|
||||
};
|
||||
|
||||
let sectionCounter = 0;
|
||||
|
||||
// Track signature validations
|
||||
let signatureTracking = {};
|
||||
let validationTimers = {};
|
||||
|
||||
// Get API URL from form data attribute
|
||||
const getApiUrl = () => {
|
||||
const form = document.querySelector('.request-form');
|
||||
return form ? form.dataset.apiUrl : 'http://localhost:8001';
|
||||
};
|
||||
|
||||
// Theme toggle functionality
|
||||
(function() {
|
||||
const STORAGE_KEY = 'theme';
|
||||
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
const saved = localStorage.getItem(STORAGE_KEY);
|
||||
|
||||
function setTheme(theme) {
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
localStorage.setItem(STORAGE_KEY, theme);
|
||||
const btn = document.getElementById('theme-toggle');
|
||||
if (btn) {
|
||||
const label = theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode';
|
||||
btn.setAttribute('aria-label', label);
|
||||
btn.title = label;
|
||||
}
|
||||
}
|
||||
|
||||
setTheme(saved || (prefersDark ? 'dark' : 'light'));
|
||||
|
||||
const btn = document.getElementById('theme-toggle');
|
||||
if (btn) {
|
||||
btn.addEventListener('click', () => {
|
||||
const current = document.documentElement.getAttribute('data-theme') === 'dark' ? 'dark' : 'light';
|
||||
setTheme(current === 'dark' ? 'light' : 'dark');
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
function addMediaType(type) {
|
||||
const btn = document.getElementById('btn-' + type);
|
||||
if (btn && btn.disabled) { return; }
|
||||
const container = document.getElementById('media-sections');
|
||||
const sectionId = 'section-' + sectionCounter++;
|
||||
|
||||
const section = document.createElement('div');
|
||||
section.className = 'media-section';
|
||||
section.id = sectionId;
|
||||
section.setAttribute('data-type', type);
|
||||
|
||||
let title = '';
|
||||
let tableHtml = '';
|
||||
|
||||
if (type === 'monografie') {
|
||||
title = 'Monografie';
|
||||
tableHtml = '<table class="data-table media-table" id="table-' + sectionId + '">' +
|
||||
'<tr>' +
|
||||
'<th>Autor<br>(Nachname, Vorname)</th>' +
|
||||
'<th>Jahr</th>' +
|
||||
'<th>Auflage</th>' +
|
||||
'<th>Titel</th>' +
|
||||
'<th>Signatur</th>' +
|
||||
'<th>Seiten von</th>' +
|
||||
'<th>Seiten bis</th>' +
|
||||
'<th></th>' +
|
||||
'</tr>' +
|
||||
'</table>';
|
||||
} else if (type === 'zeitschriftenartikel') {
|
||||
title = 'Zeitschriftenartikel';
|
||||
tableHtml = '<table class="data-table media-table" id="table-' + sectionId + '">' +
|
||||
'<tr>' +
|
||||
'<th>Autor<br>(Nachname, Vorname)</th>' +
|
||||
'<th>Jahr</th>' +
|
||||
'<th>Band</th>' +
|
||||
'<th>Titel des Artikels</th>' +
|
||||
'<th>Titel der Zeitschrift</th>' +
|
||||
'<th>Signatur</th>' +
|
||||
'<th>Seiten von</th>' +
|
||||
'<th>Seiten bis</th>' +
|
||||
'<th></th>' +
|
||||
'</tr>' +
|
||||
'</table>';
|
||||
} else if (type === 'herausgeberwerk') {
|
||||
title = 'Herausgeberwerk';
|
||||
tableHtml = '<table class="data-table media-table" id="table-' + sectionId + '">' +
|
||||
'<tr>' +
|
||||
'<th>Herausgeber<br>(Nachname, Vorname)</th>' +
|
||||
'<th>Titel des Werks</th>' +
|
||||
'<th>Jahr</th>' +
|
||||
'<th>Auflage</th>' +
|
||||
'<th>Autor des Artikels<br>(Nachname, Vorname)</th>' +
|
||||
'<th>Titel des Artikels</th>' +
|
||||
'<th>Signatur</th>' +
|
||||
'<th>Seiten von</th>' +
|
||||
'<th>Seiten bis</th>' +
|
||||
'<th></th>' +
|
||||
'</tr>' +
|
||||
'</table>';
|
||||
}
|
||||
|
||||
section.innerHTML = '<div class="media-section-header">' +
|
||||
'<h3><span class="mdi ' + getIconForType(type) + '"></span> ' + title + '</h3>' +
|
||||
'<button type="button" class="btn-icon" onclick="removeMediaSection(\'' + sectionId + '\')" title="Sektion entfernen">' +
|
||||
'<span class="mdi mdi-close"></span>' +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
tableHtml +
|
||||
'<button type="button" class="btn btn-secondary btn-sm" onclick="addMediaRow(\'' + sectionId + '\', \'' + type + '\')">' +
|
||||
'+ Eintrag hinzufügen' +
|
||||
'</button>';
|
||||
|
||||
container.appendChild(section);
|
||||
if (btn) {
|
||||
btn.disabled = true;
|
||||
btn.title = 'Sektion bereits hinzugefügt';
|
||||
}
|
||||
|
||||
addMediaRow(sectionId, type);
|
||||
}
|
||||
|
||||
function getIconForType(type) {
|
||||
const icons = {
|
||||
'monografie': 'mdi-book',
|
||||
'zeitschriftenartikel': 'mdi-newspaper',
|
||||
'herausgeberwerk': 'mdi-book-multiple'
|
||||
};
|
||||
return icons[type] || 'mdi-file';
|
||||
}
|
||||
|
||||
function addMediaRow(sectionId, type) {
|
||||
const table = document.getElementById('table-' + sectionId);
|
||||
const row = table.insertRow(-1);
|
||||
const rowId = sectionId + '-row-' + mediaCounter[type]++;
|
||||
row.id = rowId;
|
||||
|
||||
if (type === 'monografie') {
|
||||
row.innerHTML = '<td><input type="text" name="monografie_author[]" data-section="' + sectionId + '" required></td>' +
|
||||
'<td><input type="text" name="monografie_year[]" data-section="' + sectionId + '" required></td>' +
|
||||
'<td><input type="text" name="monografie_edition[]" data-section="' + sectionId + '" required></td>' +
|
||||
'<td><input type="text" name="monografie_title[]" data-section="' + sectionId + '" required></td>' +
|
||||
'<td><input type="text" name="monografie_signature[]" data-section="' + sectionId + '" placeholder="Optional"></td>' +
|
||||
'<td><input type="number" name="monografie_pages_from[]" data-section="' + sectionId + '" required min="1"></td>' +
|
||||
'<td><input type="number" name="monografie_pages_to[]" data-section="' + sectionId + '" required min="1"></td>' +
|
||||
'<td><button type="button" class="btn-icon" onclick="removeRow(\'' + rowId + '\')" title="Zeile entfernen"><span class="mdi mdi-delete"></span></button></td>';
|
||||
attachSignatureListeners(row);
|
||||
} else if (type === 'zeitschriftenartikel') {
|
||||
row.innerHTML = '<td><input type="text" name="zeitschrift_author[]" data-section="' + sectionId + '" required></td>' +
|
||||
'<td><input type="text" name="zeitschrift_year[]" data-section="' + sectionId + '" required></td>' +
|
||||
'<td><input type="text" name="zeitschrift_volume[]" data-section="' + sectionId + '" required></td>' +
|
||||
'<td><input type="text" name="zeitschrift_article_title[]" data-section="' + sectionId + '" required></td>' +
|
||||
'<td><input type="text" name="zeitschrift_journal_title[]" data-section="' + sectionId + '" required></td>' +
|
||||
'<td><input type="text" name="zeitschrift_signature[]" data-section="' + sectionId + '" required></td>' +
|
||||
'<td><input type="number" name="zeitschrift_pages_from[]" data-section="' + sectionId + '" required min="1"></td>' +
|
||||
'<td><input type="number" name="zeitschrift_pages_to[]" data-section="' + sectionId + '" required min="1"></td>' +
|
||||
'<td><button type="button" class="btn-icon" onclick="removeRow(\'' + rowId + '\')" title="Zeile entfernen"><span class="mdi mdi-delete"></span></button></td>';
|
||||
} else if (type === 'herausgeberwerk') {
|
||||
row.innerHTML = '<td><input type="text" name="herausgeber_publisher[]" data-section="' + sectionId + '" required></td>' +
|
||||
'<td><input type="text" name="herausgeber_work_title[]" data-section="' + sectionId + '" required></td>' +
|
||||
'<td><input type="text" name="herausgeber_year[]" data-section="' + sectionId + '" required></td>' +
|
||||
'<td><input type="text" name="herausgeber_edition[]" data-section="' + sectionId + '" required></td>' +
|
||||
'<td><input type="text" name="herausgeber_article_author[]" data-section="' + sectionId + '" required></td>' +
|
||||
'<td><input type="text" name="herausgeber_article_title[]" data-section="' + sectionId + '" required></td>' +
|
||||
'<td><input type="text" name="herausgeber_signature[]" data-section="' + sectionId + '" placeholder="Optional"></td>' +
|
||||
'<td><input type="number" name="herausgeber_pages_from[]" data-section="' + sectionId + '" required min="1"></td>' +
|
||||
'<td><input type="number" name="herausgeber_pages_to[]" data-section="' + sectionId + '" required min="1"></td>' +
|
||||
'<td><button type="button" class="btn-icon" onclick="removeRow(\'' + rowId + '\')" title="Zeile entfernen"><span class="mdi mdi-delete"></span></button></td>';
|
||||
attachSignatureListeners(row);
|
||||
}
|
||||
}
|
||||
|
||||
function removeRow(rowId) {
|
||||
const row = document.getElementById(rowId);
|
||||
if (row) {
|
||||
const signatureInput = row.querySelector('input[name*="_signature"]');
|
||||
if (signatureInput) {
|
||||
cleanupSignatureTracking(signatureInput);
|
||||
}
|
||||
row.remove();
|
||||
updateSubmitButton();
|
||||
}
|
||||
}
|
||||
|
||||
function validateSignature(signatureInput, pagesFromInput, pagesToInput) {
|
||||
const signature = signatureInput.value.trim();
|
||||
|
||||
if (!signature) {
|
||||
signatureInput.classList.remove('signature-validating', 'signature-valid', 'signature-invalid');
|
||||
return;
|
||||
}
|
||||
|
||||
const inputId = signatureInput.id || signatureInput.name;
|
||||
if (validationTimers[inputId]) {
|
||||
clearTimeout(validationTimers[inputId]);
|
||||
}
|
||||
|
||||
signatureInput.classList.add('signature-validating');
|
||||
signatureInput.classList.remove('signature-valid', 'signature-invalid');
|
||||
|
||||
validationTimers[inputId] = setTimeout(async () => {
|
||||
try {
|
||||
const apiUrl = getApiUrl();
|
||||
const response = await fetch(apiUrl + '/api/validate-signature?signature=' + encodeURIComponent(signature));
|
||||
const data = await response.json();
|
||||
|
||||
if (data.valid) {
|
||||
if (!signatureTracking[signature]) {
|
||||
signatureTracking[signature] = {
|
||||
totalPages: data.total_pages,
|
||||
inputs: []
|
||||
};
|
||||
} else {
|
||||
signatureTracking[signature].totalPages = data.total_pages;
|
||||
}
|
||||
|
||||
const existingIndex = signatureTracking[signature].inputs.findIndex(
|
||||
item => item.signature === signatureInput
|
||||
);
|
||||
if (existingIndex === -1) {
|
||||
signatureTracking[signature].inputs.push({
|
||||
signature: signatureInput,
|
||||
pagesFrom: pagesFromInput,
|
||||
pagesTo: pagesToInput
|
||||
});
|
||||
}
|
||||
|
||||
signatureInput.classList.remove('signature-validating');
|
||||
signatureInput.classList.add('signature-valid');
|
||||
signatureInput.title = 'Signatur gefunden: ' + data.total_pages + ' Seiten';
|
||||
|
||||
checkSignatureThreshold(signature);
|
||||
} else {
|
||||
signatureInput.classList.remove('signature-validating');
|
||||
signatureInput.classList.add('signature-invalid');
|
||||
signatureInput.title = data.error || 'Signatur nicht gefunden';
|
||||
updateSubmitButton();
|
||||
}
|
||||
} catch (error) {
|
||||
signatureInput.classList.remove('signature-validating');
|
||||
signatureInput.classList.add('signature-invalid');
|
||||
signatureInput.title = 'Validierungsfehler - API nicht erreichbar';
|
||||
updateSubmitButton();
|
||||
}
|
||||
}, 800);
|
||||
}
|
||||
|
||||
function checkSignatureThreshold(signature) {
|
||||
const tracking = signatureTracking[signature];
|
||||
if (!tracking) return;
|
||||
|
||||
let totalRequestedPages = 0;
|
||||
const threshold = Math.ceil(tracking.totalPages * 0.15);
|
||||
|
||||
tracking.inputs.forEach(item => {
|
||||
const from = parseInt(item.pagesFrom.value) || 0;
|
||||
const to = parseInt(item.pagesTo.value) || 0;
|
||||
if (from > 0 && to > 0 && to >= from) {
|
||||
totalRequestedPages += (to - from + 1);
|
||||
}
|
||||
});
|
||||
|
||||
const isOverThreshold = totalRequestedPages > threshold;
|
||||
|
||||
tracking.inputs.forEach(item => {
|
||||
const row = item.signature.closest('tr');
|
||||
if (row) {
|
||||
if (isOverThreshold) {
|
||||
row.classList.add('threshold-exceeded');
|
||||
item.signature.title = 'Warnung: Gesamtanzahl der Seiten (' + totalRequestedPages + ') überschreitet 15% (' + threshold + ' Seiten)';
|
||||
} else {
|
||||
row.classList.remove('threshold-exceeded');
|
||||
item.signature.title = 'Signatur gefunden: ' + tracking.totalPages + ' Seiten (Aktuell: ' + totalRequestedPages + '/' + threshold + ')';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
updateSubmitButton();
|
||||
}
|
||||
|
||||
function cleanupSignatureTracking(signatureInput) {
|
||||
const signature = signatureInput.value.trim();
|
||||
if (signature && signatureTracking[signature]) {
|
||||
signatureTracking[signature].inputs = signatureTracking[signature].inputs.filter(
|
||||
item => item.signature !== signatureInput
|
||||
);
|
||||
|
||||
if (signatureTracking[signature].inputs.length === 0) {
|
||||
delete signatureTracking[signature];
|
||||
} else {
|
||||
checkSignatureThreshold(signature);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateSubmitButton() {
|
||||
const submitButton = document.querySelector('button[type="submit"]');
|
||||
const hasInvalidSignatures = document.querySelectorAll('.signature-invalid').length > 0;
|
||||
const hasThresholdExceeded = document.querySelectorAll('.threshold-exceeded').length > 0;
|
||||
|
||||
if (hasInvalidSignatures || hasThresholdExceeded) {
|
||||
submitButton.disabled = true;
|
||||
if (hasThresholdExceeded) {
|
||||
submitButton.title = 'Formular kann nicht abgesendet werden: 15%-Grenze überschritten';
|
||||
} else {
|
||||
submitButton.title = 'Formular kann nicht abgesendet werden: Ungültige Signaturen';
|
||||
}
|
||||
} else {
|
||||
submitButton.disabled = false;
|
||||
submitButton.title = '';
|
||||
}
|
||||
}
|
||||
|
||||
function attachSignatureListeners(row) {
|
||||
const signatureInput = row.querySelector('input[name*="_signature"]');
|
||||
const pagesFromInput = row.querySelector('input[name*="_pages_from"]');
|
||||
const pagesToInput = row.querySelector('input[name*="_pages_to"]');
|
||||
|
||||
if (signatureInput && pagesFromInput && pagesToInput) {
|
||||
if (!signatureInput.id) {
|
||||
signatureInput.id = 'sig-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9);
|
||||
}
|
||||
|
||||
signatureInput.addEventListener('input', () => {
|
||||
const oldSignature = signatureInput.dataset.lastSignature || '';
|
||||
const newSignature = signatureInput.value.trim();
|
||||
|
||||
if (oldSignature && oldSignature !== newSignature) {
|
||||
const tempInput = document.createElement('input');
|
||||
tempInput.value = oldSignature;
|
||||
cleanupSignatureTracking(tempInput);
|
||||
}
|
||||
|
||||
signatureInput.dataset.lastSignature = newSignature;
|
||||
validateSignature(signatureInput, pagesFromInput, pagesToInput);
|
||||
});
|
||||
|
||||
pagesFromInput.addEventListener('input', () => {
|
||||
const signature = signatureInput.value.trim();
|
||||
if (signature && signatureTracking[signature]) {
|
||||
checkSignatureThreshold(signature);
|
||||
}
|
||||
});
|
||||
|
||||
pagesToInput.addEventListener('input', () => {
|
||||
const signature = signatureInput.value.trim();
|
||||
if (signature && signatureTracking[signature]) {
|
||||
checkSignatureThreshold(signature);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function removeMediaSection(sectionId) {
|
||||
const section = document.getElementById(sectionId);
|
||||
if (section) {
|
||||
if (confirm('Möchten Sie diese Sektion wirklich entfernen?')) {
|
||||
const type = section.getAttribute('data-type');
|
||||
const rows = section.querySelectorAll('tr[id]');
|
||||
rows.forEach(row => {
|
||||
const signatureInput = row.querySelector('input[name*="_signature"]');
|
||||
if (signatureInput) {
|
||||
cleanupSignatureTracking(signatureInput);
|
||||
}
|
||||
});
|
||||
section.remove();
|
||||
const btn = document.getElementById('btn-' + type);
|
||||
if (btn) {
|
||||
btn.disabled = false;
|
||||
btn.title = 'Sektion hinzufügen';
|
||||
}
|
||||
updateSubmitButton();
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
435
php/functions.php
Normal file
435
php/functions.php
Normal file
@@ -0,0 +1,435 @@
|
||||
<?php
|
||||
require_once 'config.php';
|
||||
|
||||
/**
|
||||
* Write log message to file
|
||||
*/
|
||||
function writeLog($message, $level = 'INFO')
|
||||
{
|
||||
if (!LOG_ENABLED) {
|
||||
return;
|
||||
}
|
||||
|
||||
$timestamp = date('Y-m-d H:i:s');
|
||||
$logMessage = "[{$timestamp}] [{$level}] {$message}" . PHP_EOL;
|
||||
|
||||
// Also log to error_log for immediate visibility
|
||||
error_log("[MAIL] {$message}");
|
||||
|
||||
// Write to log file
|
||||
if (defined('LOG_FILE')) {
|
||||
file_put_contents(LOG_FILE, $logMessage, FILE_APPEND | LOCK_EX);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate email address
|
||||
*/
|
||||
function validateEmail($email)
|
||||
{
|
||||
return preg_match(EMAIL_REGEX, $email);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize input string
|
||||
*/
|
||||
function sanitizeInput($input)
|
||||
{
|
||||
return htmlspecialchars(trim($input), ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate XML from form data
|
||||
*/
|
||||
function generateXML($data)
|
||||
{
|
||||
$xml = new DOMDocument('1.0', 'UTF-8');
|
||||
$xml->formatOutput = true;
|
||||
|
||||
$root = $xml->createElement('form_submission');
|
||||
$xml->appendChild($root);
|
||||
|
||||
// Static data
|
||||
$static = $xml->createElement('static');
|
||||
$root->appendChild($static);
|
||||
|
||||
foreach (['name', 'lastname', 'title', 'telno', 'mail', 'apparatsname', 'subject', 'semester', 'dauerapparat'] as $field) {
|
||||
if (isset($data[$field])) {
|
||||
$element = $xml->createElement($field, htmlspecialchars($data[$field]));
|
||||
$static->appendChild($element);
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($data['message'])) {
|
||||
$messageEl = $xml->createElement('message', htmlspecialchars($data['message']));
|
||||
$static->appendChild($messageEl);
|
||||
}
|
||||
|
||||
// Books
|
||||
if (isset($data['books']) && is_array($data['books'])) {
|
||||
$booksNode = $xml->createElement('books');
|
||||
$root->appendChild($booksNode);
|
||||
|
||||
foreach ($data['books'] as $book) {
|
||||
$bookNode = $xml->createElement('book');
|
||||
$booksNode->appendChild($bookNode);
|
||||
|
||||
foreach (['authorname', 'year', 'title', 'signature'] as $field) {
|
||||
$value = isset($book[$field]) ? htmlspecialchars($book[$field]) : '';
|
||||
$fieldNode = $xml->createElement($field, $value);
|
||||
$bookNode->appendChild($fieldNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $xml->saveXML();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate XML for ELSA form
|
||||
*/
|
||||
function generateELSAXML($data)
|
||||
{
|
||||
$xml = new DOMDocument('1.0', 'UTF-8');
|
||||
$xml->formatOutput = true;
|
||||
|
||||
$root = $xml->createElement('elsa_submission');
|
||||
$xml->appendChild($root);
|
||||
|
||||
// General info
|
||||
$generalInfo = $xml->createElement('general_info');
|
||||
$root->appendChild($generalInfo);
|
||||
|
||||
$generalFields = [
|
||||
'name',
|
||||
'lastname',
|
||||
'title',
|
||||
'mail',
|
||||
'subject',
|
||||
'classname',
|
||||
'usage_date_from',
|
||||
'usage_date_to',
|
||||
'availability_date'
|
||||
];
|
||||
|
||||
foreach ($generalFields as $field) {
|
||||
if (isset($data[$field])) {
|
||||
$element = $xml->createElement($field, htmlspecialchars($data[$field]));
|
||||
$generalInfo->appendChild($element);
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($data['message'])) {
|
||||
$messageEl = $xml->createElement('message', htmlspecialchars($data['message']));
|
||||
$generalInfo->appendChild($messageEl);
|
||||
}
|
||||
|
||||
// Media sections
|
||||
$mediaRoot = $xml->createElement('media');
|
||||
$root->appendChild($mediaRoot);
|
||||
|
||||
// Add different media types (monografie, zeitschrift, herausgeber)
|
||||
$mediaTypes = [
|
||||
'monografien' => $data['monografien'] ?? [],
|
||||
'zeitschriftenartikel' => $data['zeitschriftenartikel'] ?? [],
|
||||
'herausgeberwerke' => $data['herausgeberwerke'] ?? []
|
||||
];
|
||||
|
||||
foreach ($mediaTypes as $type => $entries) {
|
||||
if (!empty($entries)) {
|
||||
$section = $xml->createElement($type);
|
||||
$mediaRoot->appendChild($section);
|
||||
|
||||
foreach ($entries as $entry) {
|
||||
$entryNode = $xml->createElement('entry');
|
||||
$section->appendChild($entryNode);
|
||||
|
||||
foreach ($entry as $key => $value) {
|
||||
$fieldNode = $xml->createElement($key, htmlspecialchars($value));
|
||||
$entryNode->appendChild($fieldNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $xml->saveXML();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send email with XML attachment
|
||||
* Uses PHP's mail() function or SMTP if configured
|
||||
*/
|
||||
function sendEmail($subject, $xmlContent, $toEmail = null)
|
||||
{
|
||||
$to = $toEmail ?? MAIL_TO;
|
||||
$from = MAIL_FROM;
|
||||
|
||||
writeLog("==========================================================");
|
||||
writeLog("Email Send Attempt");
|
||||
writeLog("From: {$from}");
|
||||
writeLog("To: {$to}");
|
||||
writeLog("Subject: {$subject}");
|
||||
writeLog("Content Length: " . strlen($xmlContent) . " bytes");
|
||||
|
||||
if (!MAIL_ENABLED) {
|
||||
writeLog("MAIL SENDING DISABLED - Email not sent", 'WARNING');
|
||||
writeLog("XML Content:\n" . $xmlContent);
|
||||
writeLog("==========================================================");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Try using SMTP if credentials are configured
|
||||
if (SMTP_USERNAME && SMTP_PASSWORD) {
|
||||
writeLog("SMTP credentials configured, attempting SMTP send");
|
||||
$result = sendEmailSMTP($subject, $xmlContent, $to, $from);
|
||||
|
||||
if ($result) {
|
||||
writeLog("Email sent successfully via SMTP", 'SUCCESS');
|
||||
} else {
|
||||
writeLog("Email sending via SMTP failed", 'ERROR');
|
||||
}
|
||||
|
||||
writeLog("==========================================================");
|
||||
return $result;
|
||||
}
|
||||
|
||||
// Fallback to PHP mail() only if no SMTP credentials
|
||||
writeLog("No SMTP credentials configured, using PHP mail() function");
|
||||
$headers = "From: " . $from . "\r\n";
|
||||
$headers .= "Content-Type: application/xml; charset=UTF-8\r\n";
|
||||
$headers .= "X-Mailer: PHP/" . phpversion();
|
||||
|
||||
$result = mail($to, $subject, $xmlContent, $headers);
|
||||
|
||||
if ($result) {
|
||||
writeLog("Email sent successfully via mail()", 'SUCCESS');
|
||||
} else {
|
||||
writeLog("Email sending via mail() failed", 'ERROR');
|
||||
}
|
||||
|
||||
writeLog("==========================================================");
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send email via SMTP using PHPMailer (if available) or native PHP sockets
|
||||
*/
|
||||
function sendEmailSMTP($subject, $xmlContent, $to, $from)
|
||||
{
|
||||
// Try PHPMailer first if available
|
||||
if (class_exists('PHPMailer\PHPMailer\PHPMailer')) {
|
||||
writeLog("Initializing PHPMailer for SMTP send");
|
||||
writeLog("SMTP Host: " . SMTP_HOST . ":" . SMTP_PORT);
|
||||
writeLog("SMTP Encryption: " . SMTP_ENCRYPTION);
|
||||
writeLog("SMTP Username: " . SMTP_USERNAME);
|
||||
|
||||
$mail = new PHPMailer\PHPMailer\PHPMailer(true);
|
||||
|
||||
try {
|
||||
// Server settings
|
||||
$mail->isSMTP();
|
||||
$mail->Host = SMTP_HOST;
|
||||
$mail->SMTPAuth = true;
|
||||
$mail->Username = SMTP_USERNAME;
|
||||
$mail->Password = SMTP_PASSWORD;
|
||||
$mail->SMTPSecure = SMTP_ENCRYPTION;
|
||||
$mail->Port = SMTP_PORT;
|
||||
$mail->CharSet = 'UTF-8';
|
||||
|
||||
writeLog("SMTP connection configured");
|
||||
|
||||
// Recipients
|
||||
$mail->setFrom($from);
|
||||
$mail->addAddress($to);
|
||||
|
||||
writeLog("Recipients configured");
|
||||
|
||||
// Content - XML as body
|
||||
$mail->isHTML(false);
|
||||
$mail->Subject = $subject;
|
||||
$mail->Body = $xmlContent;
|
||||
$mail->ContentType = 'text/plain';
|
||||
|
||||
writeLog("Sending email via SMTP...");
|
||||
$mail->send();
|
||||
|
||||
writeLog("SMTP send() completed successfully", 'SUCCESS');
|
||||
return true;
|
||||
} catch (Exception $e) {
|
||||
writeLog("SMTP Exception: " . $e->getMessage(), 'ERROR');
|
||||
writeLog("PHPMailer ErrorInfo: " . $mail->ErrorInfo, 'ERROR');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to read SMTP multi-line response
|
||||
$readResponse = function ($socket) {
|
||||
$response = '';
|
||||
while ($line = fgets($socket, 515)) {
|
||||
$response .= $line;
|
||||
// Check if this is the last line (code followed by space, not hyphen)
|
||||
if (preg_match('/^\d{3} /', $line)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return $response;
|
||||
};
|
||||
|
||||
// Fallback to native PHP SMTP
|
||||
writeLog("PHPMailer not available, using native PHP SMTP implementation");
|
||||
writeLog("SMTP Host: " . SMTP_HOST . ":" . SMTP_PORT);
|
||||
writeLog("SMTP Encryption: " . SMTP_ENCRYPTION);
|
||||
|
||||
// Check if required transports are available
|
||||
$transports = stream_get_transports();
|
||||
writeLog("Available transports: " . implode(', ', $transports));
|
||||
|
||||
if (SMTP_ENCRYPTION === 'ssl' && !in_array('ssl', $transports)) {
|
||||
writeLog("SSL transport not available. Enable OpenSSL extension in PHP.", 'ERROR');
|
||||
writeLog("Try changing SMTP_ENCRYPTION to 'tls' and SMTP_PORT to 587 in config.php", 'ERROR');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// Build the connection string - always start with plain TCP for TLS
|
||||
if (SMTP_ENCRYPTION === 'ssl') {
|
||||
$host = 'ssl://' . SMTP_HOST;
|
||||
} else {
|
||||
$host = SMTP_HOST;
|
||||
}
|
||||
$timeout = 30;
|
||||
|
||||
writeLog("Connecting to {$host}:" . SMTP_PORT);
|
||||
$smtp = @fsockopen($host, SMTP_PORT, $errno, $errstr, $timeout);
|
||||
|
||||
if (!$smtp) {
|
||||
writeLog("Failed to connect: ({$errno}) {$errstr}", 'ERROR');
|
||||
return false;
|
||||
}
|
||||
|
||||
writeLog("Connected successfully");
|
||||
|
||||
// Read server response
|
||||
$response = $readResponse($smtp);
|
||||
writeLog("Server greeting: " . trim($response));
|
||||
|
||||
// Send EHLO
|
||||
fputs($smtp, "EHLO " . SMTP_HOST . "\r\n");
|
||||
$response = $readResponse($smtp);
|
||||
writeLog("EHLO response: " . trim(str_replace("\r\n", " | ", $response)));
|
||||
|
||||
// Start TLS if needed and not using SSL
|
||||
if (SMTP_ENCRYPTION === 'tls') {
|
||||
fputs($smtp, "STARTTLS\r\n");
|
||||
$response = $readResponse($smtp);
|
||||
writeLog("STARTTLS response: " . trim($response));
|
||||
|
||||
if (strpos($response, '220') === 0) {
|
||||
if (in_array('tls', $transports) || in_array('ssl', $transports)) {
|
||||
$crypto = @stream_socket_enable_crypto($smtp, true, STREAM_CRYPTO_METHOD_TLS_CLIENT);
|
||||
if ($crypto) {
|
||||
writeLog("TLS encryption enabled successfully");
|
||||
fputs($smtp, "EHLO " . SMTP_HOST . "\r\n");
|
||||
$response = $readResponse($smtp);
|
||||
writeLog("EHLO after TLS: " . trim(str_replace("\r\n", " | ", $response)));
|
||||
} else {
|
||||
writeLog("Failed to enable TLS encryption", 'WARNING');
|
||||
writeLog("Continuing without encryption (not recommended)", 'WARNING');
|
||||
}
|
||||
} else {
|
||||
writeLog("TLS/SSL transports not available. Enable OpenSSL extension.", 'WARNING');
|
||||
writeLog("Continuing without encryption (not recommended)", 'WARNING');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Authenticate
|
||||
fputs($smtp, "AUTH LOGIN\r\n");
|
||||
$response = $readResponse($smtp);
|
||||
writeLog("AUTH response: " . trim($response));
|
||||
|
||||
fputs($smtp, base64_encode(SMTP_USERNAME) . "\r\n");
|
||||
$response = $readResponse($smtp);
|
||||
|
||||
fputs($smtp, base64_encode(SMTP_PASSWORD) . "\r\n");
|
||||
$response = $readResponse($smtp);
|
||||
|
||||
if (strpos($response, '235') !== 0) {
|
||||
writeLog("Authentication failed: " . trim($response), 'ERROR');
|
||||
fclose($smtp);
|
||||
return false;
|
||||
}
|
||||
|
||||
writeLog("Authentication successful");
|
||||
|
||||
// Send MAIL FROM
|
||||
fputs($smtp, "MAIL FROM: <{$from}>\r\n");
|
||||
$response = $readResponse($smtp);
|
||||
writeLog("MAIL FROM response: " . trim($response));
|
||||
|
||||
// Send RCPT TO
|
||||
fputs($smtp, "RCPT TO: <{$to}>\r\n");
|
||||
$response = $readResponse($smtp);
|
||||
writeLog("RCPT TO response: " . trim($response));
|
||||
|
||||
// Send DATA
|
||||
fputs($smtp, "DATA\r\n");
|
||||
$response = $readResponse($smtp);
|
||||
writeLog("DATA response: " . trim($response));
|
||||
|
||||
// Send email headers and body
|
||||
$headers = "From: {$from}\r\n";
|
||||
$headers .= "To: {$to}\r\n";
|
||||
$headers .= "Subject: {$subject}\r\n";
|
||||
$headers .= "Content-Type: text/plain; charset=UTF-8\r\n";
|
||||
$headers .= "X-Mailer: PHP/" . phpversion() . "\r\n";
|
||||
$headers .= "\r\n";
|
||||
|
||||
fputs($smtp, $headers . $xmlContent . "\r\n.\r\n");
|
||||
$response = $readResponse($smtp);
|
||||
writeLog("Message response: " . trim($response));
|
||||
|
||||
// Quit
|
||||
fputs($smtp, "QUIT\r\n");
|
||||
$readResponse($smtp); // Read QUIT response
|
||||
fclose($smtp);
|
||||
|
||||
if (strpos($response, '250') === 0) {
|
||||
writeLog("Email sent successfully via native SMTP", 'SUCCESS');
|
||||
return true;
|
||||
} else {
|
||||
writeLog("Email sending failed: " . trim($response), 'ERROR');
|
||||
return false;
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
writeLog("Native SMTP Exception: " . $e->getMessage(), 'ERROR');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect to URL
|
||||
*/
|
||||
function redirect($url)
|
||||
{
|
||||
header("Location: " . $url);
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get POST value with default
|
||||
*/
|
||||
function post($key, $default = '')
|
||||
{
|
||||
return isset($_POST[$key]) ? sanitizeInput($_POST[$key]) : $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all POST values matching a pattern (for arrays)
|
||||
*/
|
||||
function postArray($key)
|
||||
{
|
||||
return isset($_POST[$key]) && is_array($_POST[$key]) ?
|
||||
array_map('sanitizeInput', $_POST[$key]) : [];
|
||||
}
|
||||
145
php/index.php
Normal file
145
php/index.php
Normal file
@@ -0,0 +1,145 @@
|
||||
<?php require_once 'config.php'; ?>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>PH Freiburg Bibliothek - Formulare</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="stylesheet" href="<?php echo STATIC_PATH; ?>/styles.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@7.4.47/css/materialdesignicons.min.css">
|
||||
</head>
|
||||
<body>
|
||||
<header class="site-header">
|
||||
<div class="container header-inner">
|
||||
<div class="header-left">
|
||||
<img class="logo" src="https://www.ph-freiburg.de/_assets/cc3dd7db45300ecc1f3aeed85bda5532/Images/logo-big-blue.svg" alt="PH Freiburg Logo" />
|
||||
<div class="brand">
|
||||
<div class="brand-title">Bibliothek der Pädagogischen Hochschule Freiburg</div>
|
||||
<div class="brand-sub">Hochschulbibliothek · Semesterapparatsformulare</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" id="theme-toggle" class="theme-toggle" aria-label="Switch to dark mode" title="Switch to dark mode">
|
||||
<span class="mdi mdi-theme-light-dark" aria-hidden="true"></span>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="container">
|
||||
<section class="landing-hero">
|
||||
<h1>Willkommen</h1>
|
||||
<p class="hero-subtitle">Bitte wählen Sie den gewünschten Service:</p>
|
||||
</section>
|
||||
|
||||
<div class="service-grid">
|
||||
<a href="semesterapparat.php" class="service-card">
|
||||
<div class="service-icon">
|
||||
<span class="mdi mdi-book-open-page-variant"></span>
|
||||
</div>
|
||||
<h2>Semesterapparat</h2>
|
||||
<p>Antrag für die Einrichtung eines Semesterapparats</p>
|
||||
<div class="service-arrow">
|
||||
<span class="mdi mdi-arrow-right"></span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="elsa.php" class="service-card">
|
||||
<div class="service-icon">
|
||||
<span class="mdi mdi-library-shelves"></span>
|
||||
</div>
|
||||
<h2>Elektronischer Semesterapparat (ELSA)</h2>
|
||||
<p>Antrag für die Einrichtung eines elektronischen Semesterapparats</p>
|
||||
<div class="service-arrow">
|
||||
<span class="mdi mdi-arrow-right"></span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<section class="info-section">
|
||||
<div class="card">
|
||||
<h3>Hinweise</h3>
|
||||
<p>
|
||||
Weitere Informationen zu den Semesterapparaten und elektronischen Angeboten finden Sie auf den Seiten der Hochschulbibliothek.
|
||||
</p>
|
||||
<div class="info-links">
|
||||
<a href="https://www.ph-freiburg.de/bibliothek.html" target="_blank" rel="noopener noreferrer">
|
||||
<span class="mdi mdi-open-in-new"></span> Zur Bibliothek
|
||||
</a>
|
||||
<a href="https://www.ph-freiburg.de/bibliothek/lernen/semesterapparate.html" target="_blank" rel="noopener noreferrer">
|
||||
<span class="mdi mdi-open-in-new"></span> Zu den Semesterapparaten
|
||||
</a>
|
||||
<a href="license.php" target="_blank" rel="noopener noreferrer">
|
||||
<span class="mdi mdi-file-document-outline"></span> Lizenzinformationen
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<div id="success-toast" class="toast <?php echo (defined('DEBUG_SHOW_TOAST') && DEBUG_SHOW_TOAST) || (isset($_GET['success']) && $_GET['success'] === 'true') ? '' : 'hidden'; ?>">
|
||||
<div class="toast-icon">
|
||||
<span class="mdi mdi-check-circle"></span>
|
||||
</div>
|
||||
<div class="toast-content">
|
||||
<div class="toast-title">Erfolgreich gesendet</div>
|
||||
<div class="toast-message">Mail wurde geschickt, Sie erhalten demnächst mehr Informationen</div>
|
||||
</div>
|
||||
<button class="toast-close" onclick="hideSuccessToast()" aria-label="Close">
|
||||
<span class="mdi mdi-close"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
const STORAGE_KEY = 'theme';
|
||||
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
const saved = localStorage.getItem(STORAGE_KEY);
|
||||
|
||||
function setTheme(theme) {
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
localStorage.setItem(STORAGE_KEY, theme);
|
||||
const btn = document.getElementById('theme-toggle');
|
||||
if (btn) {
|
||||
const label = theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode';
|
||||
btn.setAttribute('aria-label', label);
|
||||
btn.title = label;
|
||||
}
|
||||
}
|
||||
|
||||
setTheme(saved || (prefersDark ? 'dark' : 'light'));
|
||||
|
||||
const btn = document.getElementById('theme-toggle');
|
||||
if (btn) {
|
||||
btn.addEventListener('click', () => {
|
||||
const current = document.documentElement.getAttribute('data-theme') === 'dark' ? 'dark' : 'light';
|
||||
setTheme(current === 'dark' ? 'light' : 'dark');
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
function hideSuccessToast() {
|
||||
const toast = document.getElementById('success-toast');
|
||||
toast.classList.remove('show');
|
||||
setTimeout(() => {
|
||||
toast.classList.add('hidden');
|
||||
}, 300);
|
||||
}
|
||||
|
||||
// Show toast and auto-hide
|
||||
(function() {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const debugMode = <?php echo (defined('DEBUG_SHOW_TOAST') && DEBUG_SHOW_TOAST) ? 'true' : 'false'; ?>;
|
||||
|
||||
if (debugMode || urlParams.get('success') === 'true') {
|
||||
const toast = document.getElementById('success-toast');
|
||||
setTimeout(() => toast.classList.add('show'), 10);
|
||||
setTimeout(hideSuccessToast, 20000);
|
||||
|
||||
// Clean URL (only if not in debug mode)
|
||||
if (!debugMode) {
|
||||
const cleanUrl = window.location.pathname;
|
||||
window.history.replaceState({}, document.title, cleanUrl);
|
||||
}
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
64
php/license.php
Normal file
64
php/license.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?php require_once 'config.php'; ?>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Lizenzinformationen</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="stylesheet" href="<?php echo STATIC_PATH; ?>/styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<header class="site-header">
|
||||
<div class="container header-inner">
|
||||
<div class="header-left">
|
||||
<img class="logo" src="https://www.ph-freiburg.de/_assets/cc3dd7db45300ecc1f3aeed85bda5532/Images/logo-big-blue.svg" alt="PH Freiburg Logo" />
|
||||
<div class="brand">
|
||||
<div class="brand-title">Bibliothek der Pädagogischen Hochschule Freiburg</div>
|
||||
<div class="brand-sub">Lizenzinformationen</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" id="theme-toggle" class="theme-toggle" aria-label="Switch to dark mode" title="Switch to dark mode">
|
||||
<span class="mdi mdi-theme-light-dark" aria-hidden="true"></span>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="container">
|
||||
<section class="card">
|
||||
<h1>Lizenz & Copyright</h1>
|
||||
|
||||
<p>Please review and adjust the project license below to match your chosen license.</p>
|
||||
|
||||
<h2>Project License</h2>
|
||||
<p><a href="https://opensource.org/licenses/MIT" target="_blank" rel="noopener noreferrer">MIT</a></p>
|
||||
|
||||
<h2>Third-party assets</h2>
|
||||
<ul>
|
||||
<li>Icons: <strong>@mdi/font</strong> (Material Design Icons) — license & details: <a href="https://github.com/Templarian/MaterialDesign" target="_blank" rel="noopener noreferrer">https://github.com/Templarian/MaterialDesign</a></li>
|
||||
<li>Fonts: system and web fonts used (e.g. Segoe UI, Roboto) — please verify license terms with the respective vendors/providers.
|
||||
Useful links: <a href="https://fonts.google.com/" target="_blank" rel="noopener noreferrer">Google Fonts</a>
|
||||
</li>
|
||||
<li>Any other third-party libraries used (e.g. PHPMailer) — check their repository for license text.</li>
|
||||
</ul>
|
||||
|
||||
<p class="footer-note">
|
||||
Hinweis: Weitere Informationen finden Sie auf den verlinkten Projektseiten.
|
||||
</p>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
const STORAGE_KEY = 'theme';
|
||||
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
const saved = localStorage.getItem(STORAGE_KEY);
|
||||
|
||||
function setTheme(theme) {
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
localStorage.setItem(STORAGE_KEY, theme);
|
||||
}
|
||||
|
||||
setTheme(saved || (prefersDark ? 'dark' : 'light'));
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
200
php/semesterapparat.php
Normal file
200
php/semesterapparat.php
Normal file
@@ -0,0 +1,200 @@
|
||||
<?php require_once 'config.php'; ?>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Semesterapparatsantrag</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="stylesheet" href="<?php echo STATIC_PATH; ?>/styles.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@7.4.47/css/materialdesignicons.min.css">
|
||||
</head>
|
||||
<body>
|
||||
<header class="site-header">
|
||||
<div class="container header-inner">
|
||||
<div class="header-left">
|
||||
<img class="logo" src="https://www.ph-freiburg.de/_assets/cc3dd7db45300ecc1f3aeed85bda5532/Images/logo-big-blue.svg" alt="PH Freiburg Logo" />
|
||||
<div class="brand">
|
||||
<div class="brand-title">Bibliothek der Pädagogischen Hochschule Freiburg</div>
|
||||
<div class="brand-sub">Hochschulbibliothek · Semesterapparate</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" id="theme-toggle" class="theme-toggle" aria-label="Switch to dark mode" title="Switch to dark mode">
|
||||
<span class="mdi mdi-theme-light-dark" aria-hidden="true"></span>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="container">
|
||||
<section class="card">
|
||||
<h1>Semesterapparatsinformationen</h1>
|
||||
<form method="post" action="submit_semesterapparat.php" class="request-form">
|
||||
<div class="grid-form">
|
||||
<div class="form-field">
|
||||
<label for="name">Name</label>
|
||||
<input type="text" name="name" id="name" required>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label for="lastname">Nachname</label>
|
||||
<input type="text" name="lastname" id="lastname" required>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label for="title">Titel (optional)</label>
|
||||
<input type="text" name="title" id="title">
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label for="telno">Telefonnummer</label>
|
||||
<input type="tel" name="telno" id="telno" required>
|
||||
</div>
|
||||
|
||||
<div class="form-field">
|
||||
<label for="mail">Email</label>
|
||||
<input type="email" name="mail" id="mail" required>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label for="apparatsname">Apparatsname</label>
|
||||
<input type="text" name="apparatsname" id="apparatsname" maxlength="40" required>
|
||||
<div id="apparatsname-warning" class="field-info hidden" role="status" aria-live="polite">Name will be changed to keep in line with requirements</div>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label for="subject">Fach</label>
|
||||
<select name="subject" id="subject" required>
|
||||
<option value="">-- Auswählen --</option>
|
||||
<option>Biologie</option>
|
||||
<option>Chemie</option>
|
||||
<option>Deutsch</option>
|
||||
<option>Englisch</option>
|
||||
<option>Erziehungswirtschaft</option>
|
||||
<option>Französisch</option>
|
||||
<option>Geographie</option>
|
||||
<option>Geschichte</option>
|
||||
<option>Gesundheitspädagogik</option>
|
||||
<option>Haushalt / Textil</option>
|
||||
<option>Kunst</option>
|
||||
<option>Mathematik / Informatik</option>
|
||||
<option>Medien in der Bildung</option>
|
||||
<option>Musik</option>
|
||||
<option>Philosophie</option>
|
||||
<option>Physik</option>
|
||||
<option>Politikwissenschaft</option>
|
||||
<option>Prorektorat Lehre und Studium</option>
|
||||
<option>Psychologie</option>
|
||||
<option>Soziologie</option>
|
||||
<option>Sport</option>
|
||||
<option>Technik</option>
|
||||
<option>Theologie</option>
|
||||
<option>Wirtschaftslehre</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label>Semester</label>
|
||||
<div class="inline-controls">
|
||||
<label class="radio"><input type="radio" name="semester_type" value="SoSe" required>Sommer</label>
|
||||
<label class="radio"><input type="radio" name="semester_type" value="WiSe" required>Winter</label>
|
||||
<input type="number" name="semester_year" placeholder="Jahr" required>
|
||||
</div>
|
||||
<div class="inline-controls start" style="margin-top: 0.5rem;">
|
||||
<label class="checkbox"><input type="checkbox" name="dauerapparat" value="true"> Dauerapparat</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Medien</h2>
|
||||
<table id="book-table" class="data-table">
|
||||
<tr>
|
||||
<th>Autorenname</th><th>Jahr</th><th>Auflage</th><th>Titel</th><th>Signatur (wenn vorhanden)</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><input type="text" name="authorname[]" required></td>
|
||||
<td><input type="text" name="year[]" required></td>
|
||||
<td><input type="text" name="edition[]" required></td>
|
||||
<td><input type="text" name="title[]" required></td>
|
||||
<td><input type="text" name="signature[]"></td>
|
||||
</tr>
|
||||
</table>
|
||||
<button type="button" class="btn btn-secondary" onclick="addRow()">+ Medium hinzufügen</button>
|
||||
|
||||
<div class="form-field" style="margin-top: 20px;">
|
||||
<label for="message">Nachricht (optional)</label>
|
||||
<textarea name="message" id="message" rows="4" placeholder="Zusätzliche Anmerkungen oder Hinweise..."></textarea>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button type="submit" class="btn btn-primary">Absenden</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<p class="footer-note">
|
||||
Hinweis: Weitere Informationen zu den Semesterapparaten finden Sie auf den Seiten der Hochschulbibliothek der PH Freiburg.
|
||||
<br>
|
||||
<a href="https://www.ph-freiburg.de/bibliothek.html" target="_blank" rel="noopener noreferrer">Zur Bibliothek</a>
|
||||
|
|
||||
<a href="https://www.ph-freiburg.de/bibliothek/lernen/semesterapparate.html" target="_blank" rel="noopener noreferrer">Zu den Semesterapparaten</a>
|
||||
</p>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
function addRow() {
|
||||
const table = document.getElementById("book-table");
|
||||
const row = table.insertRow(-1);
|
||||
row.innerHTML = `
|
||||
<td><input type="text" name="authorname[]" required></td>
|
||||
<td><input type="text" name="year[]" required></td>
|
||||
<td><input type="text" name="edition[]" required></td>
|
||||
<td><input type="text" name="booktitle[]" required></td>
|
||||
<td><input type="text" name="signature[]"></td>
|
||||
`;
|
||||
}
|
||||
|
||||
(function() {
|
||||
const STORAGE_KEY = 'theme';
|
||||
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
const saved = localStorage.getItem(STORAGE_KEY);
|
||||
|
||||
function setTheme(theme) {
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
localStorage.setItem(STORAGE_KEY, theme);
|
||||
const btn = document.getElementById('theme-toggle');
|
||||
if (btn) {
|
||||
const label = theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode';
|
||||
btn.setAttribute('aria-label', label);
|
||||
btn.title = label;
|
||||
}
|
||||
}
|
||||
|
||||
setTheme(saved || (prefersDark ? 'dark' : 'light'));
|
||||
|
||||
const btn = document.getElementById('theme-toggle');
|
||||
if (btn) {
|
||||
btn.addEventListener('click', () => {
|
||||
const current = document.documentElement.getAttribute('data-theme') === 'dark' ? 'dark' : 'light';
|
||||
setTheme(current === 'dark' ? 'light' : 'dark');
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
(function() {
|
||||
function updateWarning() {
|
||||
const last = document.getElementById('lastname');
|
||||
const app = document.getElementById('apparatsname');
|
||||
const warn = document.getElementById('apparatsname-warning');
|
||||
if (!last || !app || !warn) return;
|
||||
const lastLen = (last.value || '').length;
|
||||
const appLen = (app.value || '').length;
|
||||
const threshold = Math.max(0, 37 - lastLen);
|
||||
if (appLen > threshold) {
|
||||
warn.classList.remove('hidden');
|
||||
} else {
|
||||
warn.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
document.addEventListener('input', (e) => {
|
||||
if (e.target && (e.target.id === 'lastname' || e.target.id === 'apparatsname')) {
|
||||
updateWarning();
|
||||
}
|
||||
});
|
||||
document.addEventListener('DOMContentLoaded', updateWarning);
|
||||
updateWarning();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
619
php/static/styles.css
Normal file
619
php/static/styles.css
Normal file
@@ -0,0 +1,619 @@
|
||||
:root {
|
||||
--bg: #f7f8fb;
|
||||
--card-bg: #ffffff;
|
||||
--text: #1f2937;
|
||||
--muted: #6b7280;
|
||||
--primary: #0b7bd6; /* Accessible blue */
|
||||
--primary-600: #0a6ec0;
|
||||
--primary-700: #095fa6;
|
||||
--border: #e5e7eb;
|
||||
--ring: rgba(11, 123, 214, 0.35);
|
||||
--table-head-bg: #f3f4f6;
|
||||
--input-bg: #ffffff;
|
||||
--control-accent: var(--primary);
|
||||
}
|
||||
|
||||
/* Dark theme variables */
|
||||
[data-theme="dark"] {
|
||||
--bg: #0b1220;
|
||||
--card-bg: #121a2a;
|
||||
--text: #e5e7eb;
|
||||
--muted: #9aa4b2;
|
||||
--primary: #5aa1ff;
|
||||
--primary-600: #4b90ea;
|
||||
--primary-700: #3c7ace;
|
||||
--border: #243045;
|
||||
--ring: rgba(90, 161, 255, 0.35);
|
||||
--table-head-bg: #172136;
|
||||
--input-bg: #0f1726;
|
||||
--control-accent: #2a3448;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
html, body { height: 100%; }
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji", "Segoe UI Emoji";
|
||||
color: var(--text);
|
||||
background: var(--bg);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.site-header {
|
||||
background: var(--card-bg);
|
||||
border-bottom: 1px solid var(--border);
|
||||
/* Make header fixed so it remains visible on long pages */
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1100;
|
||||
}
|
||||
.header-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
height: 72px;
|
||||
padding: 14px 0;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
}
|
||||
.logo { height: 48px; width: auto; }
|
||||
.brand-title { font-weight: 700; letter-spacing: .2px; }
|
||||
.brand-sub { color: var(--muted); font-size: .95rem; }
|
||||
|
||||
.theme-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
scale: 2;
|
||||
border-radius: 9999px;
|
||||
border: none;
|
||||
background: var(--card-bg);
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
}
|
||||
.theme-toggle:hover { filter: brightness(1.05); }
|
||||
.theme-toggle:focus-visible { outline: none; box-shadow: 0 0 0 4px var(--ring); }
|
||||
|
||||
/* Layout */
|
||||
.container {
|
||||
/* max-width: 1250px; */
|
||||
margin: 0 auto;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
/* Reserve space for fixed header so content doesn't jump under it */
|
||||
main.container {
|
||||
padding-top: 92px;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--card-bg);
|
||||
margin: 24px 0;
|
||||
padding: 22px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 8px 30px rgba(0,0,0,.05);
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0 0 14px;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 20px 0 10px;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 14px 0 10px;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
.table-wrapper { overflow-x: auto; }
|
||||
|
||||
.form-table,
|
||||
.data-table {
|
||||
width: 100%;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
background: var(--card-bg);
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.form-table::after,
|
||||
.data-table::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.form-table td,
|
||||
.form-table th {
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.data-table td,
|
||||
.data-table th {
|
||||
padding: 10px 12px;
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.form-table tr:last-child td,
|
||||
.data-table tr:last-child td { border-bottom: 0; }
|
||||
|
||||
.data-table th {
|
||||
background: var(--table-head-bg);
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Inputs */
|
||||
input[type="text"],
|
||||
input[type="email"],
|
||||
input[type="number"],
|
||||
input[type="tel"],
|
||||
input[type="date"],
|
||||
select,
|
||||
textarea {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
background: var(--input-bg);
|
||||
color: var(--text);
|
||||
outline: none;
|
||||
transition: border-color .2s ease, box-shadow .2s ease;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
input:focus,
|
||||
select:focus,
|
||||
textarea:focus {
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 4px var(--ring);
|
||||
}
|
||||
|
||||
input[type="radio"],
|
||||
input[type="checkbox"] {
|
||||
accent-color: var(--control-accent);
|
||||
}
|
||||
|
||||
.radio { margin-right: 12px; }
|
||||
|
||||
/* Buttons */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 10px 16px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid transparent;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
transition: background-color .2s ease, color .2s ease, border-color .2s ease, transform .02s ease;
|
||||
}
|
||||
|
||||
.btn:active { transform: translateY(1px); }
|
||||
|
||||
.btn-primary {
|
||||
background: var(--primary);
|
||||
color: #fff;
|
||||
}
|
||||
.btn-primary:hover { background: var(--primary-600); }
|
||||
.btn-primary:focus { box-shadow: 0 0 0 4px var(--ring); }
|
||||
|
||||
.btn-secondary {
|
||||
background: #e7eef8;
|
||||
color: var(--primary-700);
|
||||
border-color: #cfd9ea;
|
||||
}
|
||||
.btn-secondary:hover { background: #dfe8f6; }
|
||||
.btn-secondary:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
background: #e7eef8;
|
||||
color: var(--muted);
|
||||
}
|
||||
.btn-secondary:disabled:hover {
|
||||
background: #e7eef8;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .btn-secondary {
|
||||
background: #1c2637;
|
||||
color: var(--text);
|
||||
border-color: #2a3852;
|
||||
}
|
||||
[data-theme="dark"] .btn-secondary:hover { background: #1f2b3f; }
|
||||
[data-theme="dark"] .btn-secondary:disabled {
|
||||
opacity: 0.5;
|
||||
background: #1c2637;
|
||||
color: var(--muted);
|
||||
}
|
||||
[data-theme="dark"] .btn-secondary:disabled:hover {
|
||||
background: #1c2637;
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 18px;
|
||||
color: var(--muted);
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
.footer-note {
|
||||
margin: 5px;
|
||||
}
|
||||
.footer-note a { color: var(--primary-700); text-decoration: none; }
|
||||
.footer-note a:hover { text-decoration: underline; }
|
||||
|
||||
/* Static Information grid */
|
||||
.grid-form {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 12px 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.form-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
.form-field label {
|
||||
font-size: 0.9rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.field-info {
|
||||
margin-top: 4px;
|
||||
font-size: 0.85rem;
|
||||
color: var(--primary-700);
|
||||
}
|
||||
[data-theme="dark"] .field-info { color: #a9c8ff; }
|
||||
.hidden { display: none !important; }
|
||||
|
||||
.inline-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
.inline-controls.start {
|
||||
justify-content: flex-start;
|
||||
text-align: left;
|
||||
}
|
||||
.inline-controls input[type="number"] {
|
||||
width: 120px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Landing page styles */
|
||||
.landing-hero {
|
||||
text-align: center;
|
||||
margin: 40px 0 30px;
|
||||
}
|
||||
.landing-hero h1 {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.hero-subtitle {
|
||||
font-size: 1.1rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.service-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 20px;
|
||||
margin: 30px 0;
|
||||
}
|
||||
|
||||
.service-card {
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 14px;
|
||||
padding: 24px;
|
||||
text-decoration: none;
|
||||
color: var(--text);
|
||||
transition: transform .2s ease, box-shadow .2s ease, border-color .2s ease;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.service-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 12px 40px rgba(0,0,0,.1);
|
||||
border: 2px solid var(--primary);
|
||||
}
|
||||
|
||||
.service-icon {
|
||||
font-size: 3rem;
|
||||
color: var(--primary);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.service-card h2 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
.service-card p {
|
||||
color: var(--muted);
|
||||
margin: 0;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.service-arrow {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
font-size: 1.5rem;
|
||||
color: var(--primary);
|
||||
opacity: 0;
|
||||
transition: opacity .2s ease, transform .2s ease;
|
||||
}
|
||||
|
||||
.service-card:hover .service-arrow {
|
||||
opacity: 1;
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.info-section {
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.info-links {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
margin-top: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.info-links a {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: var(--primary);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.info-links a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Toast notification */
|
||||
.toast {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 8px 30px rgba(0,0,0,.15);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
max-width: 400px;
|
||||
z-index: 1000;
|
||||
opacity: 0;
|
||||
transform: translateY(-20px);
|
||||
transition: opacity .3s ease, transform .3s ease;
|
||||
}
|
||||
|
||||
.toast.show {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.toast-icon {
|
||||
font-size: 1.5rem;
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.toast-content {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.toast-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.toast-message {
|
||||
font-size: 0.9rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.toast-close {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--muted);
|
||||
cursor: pointer;
|
||||
font-size: 1.2rem;
|
||||
padding: 4px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.toast-close:hover {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 1024px) {
|
||||
.grid-form { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.header-inner { padding: 12px 0; height: auto; }
|
||||
.logo { height: 40px; }
|
||||
.form-table td:first-child { width: 40%; }
|
||||
.landing-hero h1 { font-size: 2rem; }
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.grid-form { grid-template-columns: 1fr; }
|
||||
.inline-controls { flex-wrap: wrap; }
|
||||
.toast {
|
||||
left: 10px;
|
||||
right: 10px;
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
|
||||
.data-table + .btn { margin-top: 14px; }
|
||||
|
||||
/* ELSA-specific styles */
|
||||
.legal-notice {
|
||||
background: rgba(11, 123, 214, 0.1);
|
||||
border-left: 4px solid var(--primary);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin: 20px 0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.legal-notice h3 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin: 0 0 10px;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.legal-notice p {
|
||||
margin: 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .legal-notice {
|
||||
background: rgba(90, 161, 255, 0.15);
|
||||
border-left-color: var(--primary);
|
||||
}
|
||||
|
||||
.media-controls {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin: 20px 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.media-section {
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.media-section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.media-section-header h3 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin: 0;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.media-table {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.media-table input[type="text"],
|
||||
.media-table input[type="number"] {
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--muted);
|
||||
cursor: pointer;
|
||||
padding: 6px;
|
||||
border-radius: 6px;
|
||||
transition: background-color .2s ease, color .2s ease;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.btn-icon:hover {
|
||||
background: rgba(0,0,0,.05);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .btn-icon:hover {
|
||||
background: rgba(255,255,255,.05);
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 8px 12px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* Signature validation states */
|
||||
.signature-validating {
|
||||
border-color: #f59e0b !important;
|
||||
background-image: linear-gradient(45deg, rgba(245, 158, 11, 0.1) 25%, transparent 25%, transparent 50%, rgba(245, 158, 11, 0.1) 50%, rgba(245, 158, 11, 0.1) 75%, transparent 75%, transparent);
|
||||
background-size: 20px 20px;
|
||||
animation: signature-loading 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes signature-loading {
|
||||
0% { background-position: 0 0; }
|
||||
100% { background-position: 20px 20px; }
|
||||
}
|
||||
|
||||
.signature-valid {
|
||||
border-color: #10b981 !important;
|
||||
background-color: rgba(16, 185, 129, 0.05);
|
||||
}
|
||||
|
||||
.signature-invalid {
|
||||
border-color: #ef4444 !important;
|
||||
background-color: rgba(239, 68, 68, 0.05);
|
||||
}
|
||||
|
||||
.threshold-exceeded {
|
||||
background-color: rgba(239, 68, 68, 0.08);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .threshold-exceeded {
|
||||
background-color: rgba(239, 68, 68, 0.12);
|
||||
}
|
||||
|
||||
139
php/submit_elsa.php
Normal file
139
php/submit_elsa.php
Normal file
@@ -0,0 +1,139 @@
|
||||
<?php
|
||||
require_once 'config.php';
|
||||
require_once 'functions.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
redirect('elsa.php');
|
||||
}
|
||||
|
||||
// Collect general information
|
||||
$name = post('name');
|
||||
$lastname = post('lastname');
|
||||
$title = post('title');
|
||||
$mail = post('mail');
|
||||
$subject = post('subject');
|
||||
$classname = post('classname');
|
||||
$usage_date_from = post('usage_date_from');
|
||||
$usage_date_to = post('usage_date_to');
|
||||
$availability_date = post('availability_date');
|
||||
$message = post('message');
|
||||
|
||||
// Validate required fields
|
||||
if (empty($name) || empty($lastname) || empty($mail) || empty($subject) ||
|
||||
empty($classname) || empty($usage_date_from) || empty($usage_date_to) || empty($availability_date)) {
|
||||
die('Fehler: Alle Pflichtfelder müssen ausgefüllt werden.');
|
||||
}
|
||||
|
||||
// Validate email
|
||||
if (!validateEmail($mail)) {
|
||||
die('Fehler: Ungültige E-Mail-Adresse.');
|
||||
}
|
||||
|
||||
// Collect media data
|
||||
$monografien = [];
|
||||
$zeitschriftenartikel = [];
|
||||
$herausgeberwerke = [];
|
||||
|
||||
// Process Monografie entries
|
||||
if (isset($_POST['monografie_author']) && is_array($_POST['monografie_author'])) {
|
||||
$authors = postArray('monografie_author');
|
||||
$years = postArray('monografie_year');
|
||||
$editions = postArray('monografie_edition');
|
||||
$titles = postArray('monografie_title');
|
||||
$signatures = postArray('monografie_signature');
|
||||
$pagesFrom = postArray('monografie_pages_from');
|
||||
$pagesTo = postArray('monografie_pages_to');
|
||||
|
||||
for ($i = 0; $i < count($authors); $i++) {
|
||||
$monografien[] = [
|
||||
'author' => $authors[$i] ?? '',
|
||||
'year' => $years[$i] ?? '',
|
||||
'edition' => $editions[$i] ?? '',
|
||||
'title' => $titles[$i] ?? '',
|
||||
'signature' => $signatures[$i] ?? '',
|
||||
'pages_from' => $pagesFrom[$i] ?? '',
|
||||
'pages_to' => $pagesTo[$i] ?? ''
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Process Zeitschriftenartikel entries
|
||||
if (isset($_POST['zeitschrift_author']) && is_array($_POST['zeitschrift_author'])) {
|
||||
$authors = postArray('zeitschrift_author');
|
||||
$years = postArray('zeitschrift_year');
|
||||
$volumes = postArray('zeitschrift_volume');
|
||||
$articleTitles = postArray('zeitschrift_article_title');
|
||||
$journalTitles = postArray('zeitschrift_journal_title');
|
||||
$signatures = postArray('zeitschrift_signature');
|
||||
$pagesFrom = postArray('zeitschrift_pages_from');
|
||||
$pagesTo = postArray('zeitschrift_pages_to');
|
||||
|
||||
for ($i = 0; $i < count($authors); $i++) {
|
||||
$zeitschriftenartikel[] = [
|
||||
'author' => $authors[$i] ?? '',
|
||||
'year' => $years[$i] ?? '',
|
||||
'volume' => $volumes[$i] ?? '',
|
||||
'article_title' => $articleTitles[$i] ?? '',
|
||||
'journal_title' => $journalTitles[$i] ?? '',
|
||||
'signature' => $signatures[$i] ?? '',
|
||||
'pages_from' => $pagesFrom[$i] ?? '',
|
||||
'pages_to' => $pagesTo[$i] ?? ''
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Process Herausgeberwerk entries
|
||||
if (isset($_POST['herausgeber_publisher']) && is_array($_POST['herausgeber_publisher'])) {
|
||||
$publishers = postArray('herausgeber_publisher');
|
||||
$workTitles = postArray('herausgeber_work_title');
|
||||
$years = postArray('herausgeber_year');
|
||||
$editions = postArray('herausgeber_edition');
|
||||
$articleAuthors = postArray('herausgeber_article_author');
|
||||
$articleTitles = postArray('herausgeber_article_title');
|
||||
$signatures = postArray('herausgeber_signature');
|
||||
$pagesFrom = postArray('herausgeber_pages_from');
|
||||
$pagesTo = postArray('herausgeber_pages_to');
|
||||
|
||||
for ($i = 0; $i < count($publishers); $i++) {
|
||||
$herausgeberwerke[] = [
|
||||
'publisher' => $publishers[$i] ?? '',
|
||||
'work_title' => $workTitles[$i] ?? '',
|
||||
'year' => $years[$i] ?? '',
|
||||
'edition' => $editions[$i] ?? '',
|
||||
'article_author' => $articleAuthors[$i] ?? '',
|
||||
'article_title' => $articleTitles[$i] ?? '',
|
||||
'signature' => $signatures[$i] ?? '',
|
||||
'pages_from' => $pagesFrom[$i] ?? '',
|
||||
'pages_to' => $pagesTo[$i] ?? ''
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare data for XML generation
|
||||
$data = [
|
||||
'name' => $name,
|
||||
'lastname' => $lastname,
|
||||
'title' => $title,
|
||||
'mail' => $mail,
|
||||
'subject' => $subject,
|
||||
'classname' => $classname,
|
||||
'usage_date_from' => $usage_date_from,
|
||||
'usage_date_to' => $usage_date_to,
|
||||
'availability_date' => $availability_date,
|
||||
'message' => $message,
|
||||
'monografien' => $monografien,
|
||||
'zeitschriftenartikel' => $zeitschriftenartikel,
|
||||
'herausgeberwerke' => $herausgeberwerke
|
||||
];
|
||||
|
||||
// Generate XML
|
||||
$xml = generateELSAXML($data);
|
||||
|
||||
// Send email
|
||||
$emailSent = sendEmail('New ELSA Form Submission', $xml);
|
||||
|
||||
if ($emailSent || !MAIL_ENABLED) {
|
||||
redirect('index.php?success=true');
|
||||
} else {
|
||||
die('Fehler: E-Mail konnte nicht gesendet werden. Bitte versuchen Sie es später erneut.');
|
||||
}
|
||||
84
php/submit_semesterapparat.php
Normal file
84
php/submit_semesterapparat.php
Normal file
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
require_once 'config.php';
|
||||
require_once 'functions.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
redirect('semesterapparat.php');
|
||||
}
|
||||
|
||||
// Collect form data
|
||||
$name = post('name');
|
||||
$lastname = post('lastname');
|
||||
$title = post('title');
|
||||
$telno = post('telno');
|
||||
$mail = post('mail');
|
||||
$apparatsname = post('apparatsname');
|
||||
$subject = post('subject');
|
||||
$semester_type = post('semester_type');
|
||||
$semester_year = post('semester_year');
|
||||
$dauerapparat = isset($_POST['dauerapparat']) ? 'true' : 'false';
|
||||
$message = post('message');
|
||||
|
||||
// Validate required fields
|
||||
if (
|
||||
empty($name) || empty($lastname) || empty($telno) || empty($mail) ||
|
||||
empty($apparatsname) || empty($subject) || empty($semester_type) || empty($semester_year)
|
||||
) {
|
||||
die('Fehler: Alle Pflichtfelder müssen ausgefüllt werden.');
|
||||
}
|
||||
|
||||
// Validate email
|
||||
if (!validateEmail($mail)) {
|
||||
die('Fehler: Ungültige E-Mail-Adresse.');
|
||||
}
|
||||
|
||||
// Collect book data
|
||||
$authornames = postArray('authorname');
|
||||
$years = postArray('year');
|
||||
$edition = postArray('edition');
|
||||
$booktitles = postArray('booktitle');
|
||||
$signatures = postArray('signature');
|
||||
|
||||
// Validate at least one book
|
||||
if (empty($authornames) || count($authornames) === 0) {
|
||||
die('Fehler: Mindestens ein Medium muss angegeben werden.');
|
||||
}
|
||||
|
||||
// Build books array
|
||||
$books = [];
|
||||
for ($i = 0; $i < count($authornames); $i++) {
|
||||
$books[] = [
|
||||
'authorname' => $authornames[$i] ?? '',
|
||||
'year' => $years[$i] ?? '',
|
||||
'edition' => $edition[$i] ?? '',
|
||||
'title' => $booktitles[$i] ?? '',
|
||||
'signature' => $signatures[$i] ?? ''
|
||||
];
|
||||
}
|
||||
|
||||
// Prepare data for XML generation
|
||||
$data = [
|
||||
'name' => $name,
|
||||
'lastname' => $lastname,
|
||||
'title' => $title,
|
||||
'telno' => $telno,
|
||||
'mail' => $mail,
|
||||
'apparatsname' => $apparatsname,
|
||||
'subject' => $subject,
|
||||
'semester' => $semester_type . ' ' . $semester_year,
|
||||
'dauerapparat' => $dauerapparat,
|
||||
'message' => $message,
|
||||
'books' => $books
|
||||
];
|
||||
|
||||
// Generate XML
|
||||
$xml = generateXML($data);
|
||||
|
||||
// Send email
|
||||
$emailSent = sendEmail('Antrag für neuen Semesterapparat', $xml);
|
||||
|
||||
if ($emailSent || !MAIL_ENABLED) {
|
||||
redirect('index.php?success=true');
|
||||
} else {
|
||||
die('Fehler: E-Mail konnte nicht gesendet werden. Bitte versuchen Sie es später erneut.');
|
||||
}
|
||||
Reference in New Issue
Block a user