16 Commits

Author SHA1 Message Date
0a25479e70 chore: fix header notice text 2025-11-26 15:21:04 +01:00
85a5ce8221 style: change various settings 2025-11-26 15:19:24 +01:00
ffc7877b0e feat: add footer note 2025-11-26 15:18:57 +01:00
2e9cb4d6af chore: remove link in header note 2025-11-26 15:15:42 +01:00
d099a33191 feat: add license page WIP 2025-11-26 15:15:21 +01:00
4d9fec13dd fix: add missing edition table column 2025-11-26 15:14:59 +01:00
286a91b125 chore: bump up setup to v7 2025-11-26 15:13:57 +01:00
738c72ebf2 feat: widen container to no limit, split year/edition in semap page 2025-11-26 10:22:47 +01:00
97872ef9fb style: align service card text in center instead of left 2025-11-25 14:29:59 +01:00
22b3fe9d0b feat: add OpenSSL setup guide and enhance email logging functionality 2025-11-24 14:55:57 +01:00
3a2926675a chore: update 2025-11-20 15:08:29 +01:00
74c8eacbf2 feat: add catalogue check by creating a small service runner 2025-11-20 13:04:42 +01:00
9ab4fcbe81 feat: refactor into php for mrbs page 2025-11-20 10:59:57 +01:00
f76a8d2efc fix: always compute repo name
All checks were successful
Docker Build (PR) / Build Docker image (pull_request) Has been skipped
/ build (pull_request) Successful in 25m38s
2025-11-19 20:11:04 +01:00
51f5aefcaa fix: add no cache option
Some checks failed
Docker Build (PR) / Build Docker image (pull_request) Has been skipped
/ build (pull_request) Has been cancelled
2025-11-19 19:39:31 +01:00
5c4a2a8ffc feat: update release workflow to auto create release based on PR
All checks were successful
Docker Build (PR) / Build Docker image (pull_request) Has been skipped
/ build (pull_request) Has been skipped
2025-11-19 19:24:55 +01:00
16 changed files with 2777 additions and 7 deletions

View File

@@ -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:

View File

@@ -18,12 +18,39 @@ on:
- "major"
- "minor"
- "patch"
pull_request:
types: [closed]
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
# Only run on merged PRs with [release-*] or manual workflow_dispatch
if: |
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'pull_request' && github.event.pull_request.merged == true && contains(github.event.pull_request.title, '[release-'))
steps:
- name: Determine bump type
id: bump_type
run: |
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
echo "type=${{ github.event.inputs.bump }}" >> $GITHUB_OUTPUT
else
# Extract bump type from PR title: [release-patch], [release-minor], [release-major]
PR_TITLE="${{ github.event.pull_request.title }}"
if [[ "$PR_TITLE" =~ \[release-major\] ]]; then
echo "type=major" >> $GITHUB_OUTPUT
elif [[ "$PR_TITLE" =~ \[release-minor\] ]]; then
echo "type=minor" >> $GITHUB_OUTPUT
elif [[ "$PR_TITLE" =~ \[release-patch\] ]]; then
echo "type=patch" >> $GITHUB_OUTPUT
else
echo "type=patch" >> $GITHUB_OUTPUT
fi
fi
- name: Checkout code
uses: actions/checkout@v4.2.2
with:
@@ -31,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:
@@ -52,7 +79,7 @@ jobs:
id: bump
run: |
uv tool install bump-my-version
uv tool run bump-my-version bump ${{ github.event.inputs.bump }}
uv tool run bump-my-version bump ${{ steps.bump_type.outputs.type }}
# echo the version to github env, the version is shown by using uv tool run bump-my-version show current_version
echo "VERSION<<EOF" >> $GITHUB_ENV
echo "$(uv tool run bump-my-version show current_version)" >> $GITHUB_ENV
@@ -77,12 +104,12 @@ jobs:
echo "tag=$prev" >> "$GITHUB_OUTPUT"
- name: Compute lowercased image repo
if: ${{ github.event.inputs.docker_release == 'true' }}
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.docker_release == 'true' || github.event_name == 'pull_request' }}
run: |
echo "IMAGE_REPO=${{ secrets.REGISTRY }}/$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV
- name: Docker meta
if: ${{ github.event.inputs.docker_release == 'true' }}
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.docker_release == 'true' || github.event_name == 'pull_request' }}
id: meta
uses: docker/metadata-action@v5
with:
@@ -92,12 +119,13 @@ jobs:
type=raw,value=${{ env.VERSION }}
- name: Build and push Docker image
if: ${{ github.event.inputs.docker_release == 'true' }}
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.docker_release == 'true' || github.event_name == 'pull_request' }}
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64
push: true
no-cache: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
@@ -118,7 +146,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITEA_TOKEN }}
- name: Create Gitea Release
if: ${{ github.event.inputs.github_release == 'true' }}
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.github_release == 'true' || github.event_name == 'pull_request' }}
uses: softprops/action-gh-release@v1
with:
tag_name: v${{ env.VERSION }}

97
API_SERVICE.md Normal file
View 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
View 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
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
php/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
php/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.

35
php/config.php Normal file
View 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
View 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>
&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
php/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
php/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
php/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
php/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
php/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: 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
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.');
}