initial release

This commit is contained in:
2025-12-09 10:07:14 +01:00
commit d9448a673c
13 changed files with 2670 additions and 0 deletions

25
.htaccess Normal file
View 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
OPENSSL_SETUP.md Normal file
View 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
README.md Normal file
View 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.

74
config.php Normal file
View File

@@ -0,0 +1,74 @@
<?php
/**
* Configuration file for SemapForm PHP Application
*
* Loads configuration from .env file
*/
// Load .env file
function loadEnv($path) {
if (!file_exists($path)) {
return;
}
$lines = file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
// Skip comments
if (strpos(trim($line), '#') === 0) {
continue;
}
// Parse KEY=VALUE
if (strpos($line, '=') !== false) {
list($key, $value) = explode('=', $line, 2);
$key = trim($key);
$value = trim($value);
// Remove quotes if present
if (preg_match('/^(["\'])(.*)\\1$/', $value, $matches)) {
$value = $matches[2];
}
// Set environment variable if not already set
if (!getenv($key)) {
putenv("$key=$value");
$_ENV[$key] = $value;
$_SERVER[$key] = $value;
}
}
}
}
// Load environment variables from .env file
loadEnv(__DIR__ . '/.env');
// Error reporting (set to 0 in production)
error_reporting(E_ALL);
ini_set('display_errors', 1);
// Email configuration
define('MAIL_ENABLED', getenv('MAIL_ENABLED') !== false ? filter_var(getenv('MAIL_ENABLED'), FILTER_VALIDATE_BOOLEAN) : true);
define('SMTP_HOST', getenv('SMTP_HOST') ?: 'smtp.ph-freiburg.de');
define('SMTP_PORT', getenv('SMTP_PORT') ?: 587); // Use 587 for TLS, 465 for SSL
define('SMTP_ENCRYPTION', getenv('SMTP_ENCRYPTION') ?: 'tls'); // 'ssl' or 'tls' or '' for none
define('SMTP_USERNAME', getenv('SMTP_USERNAME') ?: 'None');
define('SMTP_PASSWORD', getenv('SMTP_PASSWORD') ?: 'None');
define('MAIL_FROM', getenv('MAIL_FROM') ?: 'your_mail_here@example.com');
define('MAIL_TO', getenv('MAIL_TO') ?: 'receiver_mail@example.com');
// Application settings
define('BASE_PATH', __DIR__);
define('STATIC_PATH', '/static');
// Logging configuration
define('LOG_FILE', BASE_PATH . '/mail.log');
define('LOG_ENABLED', false);
// Debug settings
define('DEBUG_SHOW_TOAST', false); // Set to true to always show success toast for styling
// Signature validation API (optional Python service)
define('SIGNATURE_API_URL', 'https://api.theprivateserver.de/library');
// Email regex pattern
define('EMAIL_REGEX', '/^[^@\s]+@[^@\s]+\.[^@\s]+$/');

526
elsa.php Normal file
View File

@@ -0,0 +1,526 @@
<?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>
<span class="mdi mdi-information-outline info-icon"></span> Fügen Sie hier die gewünschten Medientypen hinzu. Für jeden Medientyp können Sie mehrere Einträge anlegen.
<br>
<span class="mdi mdi-information-outline info-icon"></span> Sollte ein Beitrag mehrere AutorInnen oder EditorInnen haben, sollten diese mit einem ; getrennt werden.
<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>
&nbsp;|&nbsp;
<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
functions.php Normal file
View 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
index.php Normal file
View 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
license.php Normal file
View 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
semesterapparat.php Normal file
View 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>
&nbsp;|&nbsp;
<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
static/styles.css Normal file
View 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: 92px;
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
submit_elsa.php Normal file
View 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.');
}

View 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.');
}

56
test_email.php Normal file
View File

@@ -0,0 +1,56 @@
<?php
/**
* Test script for email functionality
* Run from command line: php test_email.php
*/
require_once 'config.php';
require_once 'functions.php';
echo "=== Email Configuration Test ===\n\n";
echo "MAIL_ENABLED: " . (MAIL_ENABLED ? 'Yes' : 'No') . "\n";
echo "SMTP_HOST: " . SMTP_HOST . "\n";
echo "SMTP_PORT: " . SMTP_PORT . "\n";
echo "SMTP_ENCRYPTION: " . SMTP_ENCRYPTION . "\n";
echo "SMTP_USERNAME: " . SMTP_USERNAME . "\n";
echo "MAIL_FROM: " . MAIL_FROM . "\n";
echo "MAIL_TO: " . MAIL_TO . "\n";
echo "PHPMailer available: " . (class_exists('PHPMailer\PHPMailer\PHPMailer') ? 'Yes' : 'No') . "\n\n";
echo "=== Sending Test Email ===\n\n";
$testXML = <<<XML
<?xml version="1.0" encoding="UTF-8"?>
<test_submission>
<message>This is a test email from SemapForm PHP</message>
<timestamp>%s</timestamp>
</test_submission>
XML;
$testXML = sprintf($testXML, date('Y-m-d H:i:s'));
$subject = "SemapForm Test Email - " . date('Y-m-d H:i:s');
echo "Attempting to send test email...\n";
echo "Subject: {$subject}\n\n";
$result = sendEmail($subject, $testXML);
if ($result) {
echo "\n✓ Email sent successfully!\n";
echo "Check the recipient inbox and mail.log for details.\n";
} else {
echo "\n✗ Email sending failed!\n";
echo "Check mail.log for error details.\n";
}
echo "\n=== Log File Contents ===\n\n";
if (file_exists(LOG_FILE)) {
echo "Last 20 lines of mail.log:\n";
echo "----------------------------------------\n";
$lines = file(LOG_FILE);
$lastLines = array_slice($lines, -20);
echo implode('', $lastLines);
} else {
echo "Log file not found at: " . LOG_FILE . "\n";
}