mirror of
https://git.hexahost.dev/smueller/HexaHost-Frontend.git
synced 2026-06-02 13:28:43 +00:00
Compare commits
31 Commits
dev
...
45a7067878
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
45a7067878 | ||
|
|
4787d7b770 | ||
|
|
a0aa8b12ca | ||
|
|
b113bdeaa2 | ||
|
|
5d2be60dfa | ||
|
|
62d0076799 | ||
|
|
e920fdfc8e | ||
|
|
5d953fda7b | ||
|
|
6ca4786955 | ||
|
|
b9bd339607 | ||
|
|
b893272d64 | ||
|
|
3dd707ab93 | ||
|
|
cc1a48943a | ||
|
|
dfc781f3ed | ||
|
|
d0e5baa443 | ||
|
|
8afba16905 | ||
|
|
96a5977283 | ||
|
|
ec8686761c | ||
|
|
d3da589a1d | ||
|
|
b6e268855e | ||
|
|
d02377c735 | ||
|
|
d62d6b576d | ||
|
|
2c0138f55d | ||
|
|
2074707c9d | ||
|
|
55f9fdd957 | ||
|
|
ab81d1c49f | ||
|
|
b40ad53d9c | ||
|
|
e5402189ea | ||
|
|
e544720900 | ||
| a5bba86db0 | |||
| d34dbbb079 |
@@ -1,62 +0,0 @@
|
|||||||
name: Release Build (ci → main)
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- ci
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
env:
|
|
||||||
GITEA_HOST: git.hexahost.dev
|
|
||||||
REPO_PATH: smueller/HexaHost-Frontend
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
release-build:
|
|
||||||
if: ${{ !contains(github.event.head_commit.message, '[skip ci]') }}
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout ci (Integration)
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
repository-url: https://git.hexahost.dev/smueller/HexaHost-Frontend
|
|
||||||
ref: ci
|
|
||||||
|
|
||||||
- name: Setup Python
|
|
||||||
uses: actions/setup-python@v5
|
|
||||||
with:
|
|
||||||
python-version: "3.12"
|
|
||||||
|
|
||||||
- name: Setup Node
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: "20"
|
|
||||||
|
|
||||||
- name: Run release obfuscation
|
|
||||||
run: python scripts/obfuscate_release.py --root . --hash-assets
|
|
||||||
|
|
||||||
- name: Publish release to main
|
|
||||||
env:
|
|
||||||
GITEA_TOKEN: ${{ github.token }}
|
|
||||||
run: |
|
|
||||||
git config user.name "gitea-actions"
|
|
||||||
git config user.email "actions@local"
|
|
||||||
git remote set-url origin "https://oauth2:${GITEA_TOKEN}@${GITEA_HOST}/${REPO_PATH}.git"
|
|
||||||
git fetch origin main ci
|
|
||||||
|
|
||||||
git add -A
|
|
||||||
if git diff --cached --quiet; then
|
|
||||||
echo "No release changes to publish."
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
TREE=$(git write-tree)
|
|
||||||
MSG="chore(release): obfuscate and hash production assets [skip ci]"
|
|
||||||
if git show-ref --verify --quiet refs/remotes/origin/main; then
|
|
||||||
PARENT=$(git rev-parse origin/main)
|
|
||||||
COMMIT=$(git commit-tree "$TREE" -p "$PARENT" -m "$MSG")
|
|
||||||
else
|
|
||||||
COMMIT=$(git commit-tree "$TREE" -m "$MSG")
|
|
||||||
fi
|
|
||||||
git push origin "${COMMIT}:refs/heads/main"
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
# Validiert Commit-Messages nach Conventional Commits
|
|
||||||
# https://www.conventionalcommits.org/
|
|
||||||
|
|
||||||
commit_msg_file="$1"
|
|
||||||
|
|
||||||
first_line=$(sed '/^#/d;/^$/d' "$commit_msg_file" | head -n 1)
|
|
||||||
|
|
||||||
# Merge/Revert von Git erlauben
|
|
||||||
case "$first_line" in
|
|
||||||
Merge\ *|Revert\ *)
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
pattern='^(feat|fix|docs|style|refactor|perf|test|build|ci|chore)(\([a-z0-9._-]+\))?!?: .+'
|
|
||||||
|
|
||||||
if ! printf '%s\n' "$first_line" | grep -qE "$pattern"; then
|
|
||||||
echo >&2 "❌ Commit-Message entspricht nicht Conventional Commits."
|
|
||||||
echo >&2 ""
|
|
||||||
echo >&2 " Format: type(scope): description"
|
|
||||||
echo >&2 " Beispiel: feat(products): hide vpc in navigation"
|
|
||||||
echo >&2 ""
|
|
||||||
echo >&2 " Erlaubte types: feat, fix, docs, style, refactor, perf, test, build, ci, chore"
|
|
||||||
echo >&2 " Überspringen: git commit --no-verify"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
exit 0
|
|
||||||
63
.github/workflows/obfuscate-main.yml
vendored
63
.github/workflows/obfuscate-main.yml
vendored
@@ -1,63 +0,0 @@
|
|||||||
# Hinweis: Gitea nutzt .gitea/workflows/obfuscate-main.yml (identischer Ablauf).
|
|
||||||
name: Release Build (ci → main)
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- ci
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
env:
|
|
||||||
GITEA_HOST: git.hexahost.dev
|
|
||||||
REPO_PATH: smueller/HexaHost-Frontend
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
release-build:
|
|
||||||
if: ${{ !contains(github.event.head_commit.message, '[skip ci]') }}
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout ci (Integration)
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
repository-url: https://git.hexahost.dev/smueller/HexaHost-Frontend
|
|
||||||
ref: ci
|
|
||||||
|
|
||||||
- name: Setup Python
|
|
||||||
uses: actions/setup-python@v5
|
|
||||||
with:
|
|
||||||
python-version: "3.12"
|
|
||||||
|
|
||||||
- name: Setup Node
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: "20"
|
|
||||||
|
|
||||||
- name: Run release obfuscation
|
|
||||||
run: python scripts/obfuscate_release.py --root . --hash-assets
|
|
||||||
|
|
||||||
- name: Publish release to main
|
|
||||||
env:
|
|
||||||
GITEA_TOKEN: ${{ github.token }}
|
|
||||||
run: |
|
|
||||||
git config user.name "gitea-actions"
|
|
||||||
git config user.email "actions@local"
|
|
||||||
git remote set-url origin "https://oauth2:${GITEA_TOKEN}@${GITEA_HOST}/${REPO_PATH}.git"
|
|
||||||
git fetch origin main ci
|
|
||||||
|
|
||||||
git add -A
|
|
||||||
if git diff --cached --quiet; then
|
|
||||||
echo "No release changes to publish."
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
TREE=$(git write-tree)
|
|
||||||
MSG="chore(release): obfuscate and hash production assets [skip ci]"
|
|
||||||
if git show-ref --verify --quiet refs/remotes/origin/main; then
|
|
||||||
PARENT=$(git rev-parse origin/main)
|
|
||||||
COMMIT=$(git commit-tree "$TREE" -p "$PARENT" -m "$MSG")
|
|
||||||
else
|
|
||||||
COMMIT=$(git commit-tree "$TREE" -m "$MSG")
|
|
||||||
fi
|
|
||||||
git push origin "${COMMIT}:refs/heads/main"
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -13,7 +13,6 @@ build/
|
|||||||
# Environment variables
|
# Environment variables
|
||||||
.env
|
.env
|
||||||
.cursorrules
|
.cursorrules
|
||||||
.cursor/
|
|
||||||
.cursorrules.txt
|
.cursorrules.txt
|
||||||
.env.local
|
.env.local
|
||||||
.env.development.local
|
.env.development.local
|
||||||
|
|||||||
16
.gitmessage
16
.gitmessage
@@ -1,16 +0,0 @@
|
|||||||
# Conventional Commits – nur die erste nicht-kommentierte Zeile wird verwendet
|
|
||||||
# Format: type(scope): description
|
|
||||||
#
|
|
||||||
# feat Neues Feature
|
|
||||||
# fix Bugfix
|
|
||||||
# docs Dokumentation
|
|
||||||
# style Formatierung (keine Logik)
|
|
||||||
# refactor Umbau ohne Feature/Fix
|
|
||||||
# perf Performance
|
|
||||||
# test Tests
|
|
||||||
# build Build / Dependencies
|
|
||||||
# ci CI/CD
|
|
||||||
# chore Sonstiges
|
|
||||||
#
|
|
||||||
# Beispiel (diese Zeile anpassen und Kommentare löschen oder stehen lassen):
|
|
||||||
# feat(products): hide vpc and vps in navigation
|
|
||||||
48
README.md
48
README.md
@@ -166,51 +166,25 @@ Für den Produktivbetrieb `public/` als Webroot konfigurieren.
|
|||||||
|
|
||||||
### Production-Build & Veröffentlichung
|
### Production-Build & Veröffentlichung
|
||||||
|
|
||||||
| Branch | Zweck |
|
Der Quellcode bleibt auf `dev`, der veröffentlichte Stand liegt auf `main` (ohne Kommentare, obfuskiertes JS).
|
||||||
|--------|--------|
|
|
||||||
| **`dev`** | Entwicklung (lesbarer Code, Kommentare) |
|
|
||||||
| **`ci`** | Integration (du mergst `dev` hierher) |
|
|
||||||
| **`main`** | Release/Produktion (obfuskiert, gehashte Assets — nur per Pipeline) |
|
|
||||||
|
|
||||||
**Ablauf: `dev` → `ci` → `main`**
|
**Voraussetzungen:** Node.js 18+ (inkl. npm), PHP 8+ CLI, Git
|
||||||
|
|
||||||
1. Auf **`dev`** entwickeln und pushen
|
|
||||||
2. **`dev` nach `ci` mergen** (manuell, z. B. in Gitea oder lokal)
|
|
||||||
3. **`ci` pushen** → startet `.gitea/workflows/obfuscate-main.yml`
|
|
||||||
4. Pipeline obfuskiert im Runner-Workspace und publiziert nach **`main`**
|
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Nach fertigen Änderungen auf dev:
|
# Windows
|
||||||
git checkout ci
|
.\scripts\run-build.ps1
|
||||||
git pull origin ci
|
.\scripts\publish-to-main.ps1 -Push
|
||||||
git merge dev
|
|
||||||
git push origin ci
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Bei jedem Push auf **`ci`**:
|
|
||||||
|
|
||||||
1. Checkout von `ci` im temporären Runner-Workspace
|
|
||||||
2. Obfuscation-Build (`scripts/obfuscate_release.py --hash-assets`)
|
|
||||||
3. Ergebnis nach `main` pushen (Bot-Commit mit `[skip ci]`)
|
|
||||||
|
|
||||||
**Nicht** `dev` oder `ci` direkt nach `main` mergen. Der Branch **`ci` bleibt lesbar** — Obfuscation wird nur nach `main` publiziert.
|
|
||||||
|
|
||||||
`ci`-Branch einmalig anlegen (falls noch nicht vorhanden): `git checkout -b ci dev && git push -u origin ci`
|
|
||||||
|
|
||||||
Der Build führt aus:
|
|
||||||
|
|
||||||
- Entfernen von Kommentaren (inkl. Block-Kommentaren) in PHP/JS/CSS
|
|
||||||
- Minify + Obfuscate für JavaScript
|
|
||||||
- Minify für CSS
|
|
||||||
- Kein Source-Map-Output
|
|
||||||
- Hashing von JS/CSS-Dateinamen + automatische Referenz-Anpassung
|
|
||||||
|
|
||||||
Lokal testen (nur in Kopie, nicht committen):
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python scripts/obfuscate_release.py --root . --hash-assets
|
# Linux / macOS
|
||||||
|
chmod +x scripts/*.sh
|
||||||
|
./scripts/run-build.sh
|
||||||
|
./scripts/publish-to-main.sh --push
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Details: `scripts/build/README.md`
|
||||||
|
|
||||||
## 🔗 Backend-Integration
|
## 🔗 Backend-Integration
|
||||||
|
|
||||||
Das Backend-Repository enthält folgende wiederverwendbare Komponenten:
|
Das Backend-Repository enthält folgende wiederverwendbare Komponenten:
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ define('SMTP_TO_EMAIL', 'info@hexahost.de'); // Empfänger-E-Mail für Kon
|
|||||||
// Sicherheitseinstellungen
|
// Sicherheitseinstellungen
|
||||||
define('ENABLE_CSRF_PROTECTION', true); // CSRF-Schutz aktivieren
|
define('ENABLE_CSRF_PROTECTION', true); // CSRF-Schutz aktivieren
|
||||||
define('ENABLE_RATE_LIMITING', true); // Rate-Limiting aktivieren
|
define('ENABLE_RATE_LIMITING', true); // Rate-Limiting aktivieren
|
||||||
define('MAX_REQUESTS_PER_HOUR', 5); // Max. Anfragen pro Stunde
|
define('MAX_REQUESTS_PER_HOUR', 10); // Max. Anfragen pro Stunde
|
||||||
|
|
||||||
// Spam-Schutz Einstellungen
|
// Spam-Schutz Einstellungen
|
||||||
define('ENABLE_SPAM_PROTECTION', true); // Spam-Schutz aktivieren
|
define('ENABLE_SPAM_PROTECTION', true); // Spam-Schutz aktivieren
|
||||||
|
|||||||
@@ -443,43 +443,10 @@ $PRODUCTS['webhosting'] = [
|
|||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
// Sichtbarkeit in Navigation, Footer und auf der Startseite (Seiten bleiben per URL erreichbar)
|
|
||||||
$PRODUCT_VISIBILITY = [
|
|
||||||
'vpc' => false,
|
|
||||||
'vps' => false,
|
|
||||||
'mail-gateway' => false,
|
|
||||||
'webhosting' => true,
|
|
||||||
];
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// HILFSFUNKTIONEN
|
// HILFSFUNKTIONEN
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
/**
|
|
||||||
* Prüft, ob eine Produktkategorie in der Navigation angezeigt wird
|
|
||||||
*/
|
|
||||||
function isProductVisible(string $productId): bool {
|
|
||||||
global $PRODUCT_VISIBILITY;
|
|
||||||
return $PRODUCT_VISIBILITY[$productId] ?? true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* HTML hidden-Attribut für ausgeblendete Produktkategorien
|
|
||||||
*/
|
|
||||||
function productHiddenAttr(string $productId): string {
|
|
||||||
return isProductVisible($productId) ? '' : ' hidden';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Aktive Navigationsseiten für sichtbare Produktkategorien
|
|
||||||
*
|
|
||||||
* @return string[]
|
|
||||||
*/
|
|
||||||
function getVisibleProductPageIds(): array {
|
|
||||||
global $PRODUCT_VISIBILITY;
|
|
||||||
return array_keys(array_filter($PRODUCT_VISIBILITY, static fn(bool $visible): bool => $visible));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Alle Produkte abrufen
|
* Alle Produkte abrufen
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -15,17 +15,10 @@
|
|||||||
<div class="footer-section">
|
<div class="footer-section">
|
||||||
<h4>Produkte</h4>
|
<h4>Produkte</h4>
|
||||||
<ul>
|
<ul>
|
||||||
<li<?php echo productHiddenAttr('vpc'); ?>><a href="/vpc">Virtual Private Container</a></li>
|
<li><a href="/vpc">Virtual Private Container</a></li>
|
||||||
<li<?php echo productHiddenAttr('vps'); ?>><a href="/vps">Virtual Private Server</a></li>
|
<li><a href="/vps">Virtual Private Server</a></li>
|
||||||
<li<?php echo productHiddenAttr('mail-gateway'); ?>><a href="/mail-gateway">Mail Gateway</a></li>
|
<li><a href="/mail-gateway">Mail Gateway</a></li>
|
||||||
<li<?php echo productHiddenAttr('webhosting'); ?>><a href="/webhosting">Webhosting</a></li>
|
<li><a href="/webhosting">Webhosting</a></li>
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="footer-section">
|
|
||||||
<h4>Andere Dienste</h4>
|
|
||||||
<ul>
|
|
||||||
<li><a href="https://hexadns.de" target="_blank" rel="noopener noreferrer">hexadns.de</a></li>
|
|
||||||
<li><a href="https://www.hexa-mail.de/" target="_blank" rel="noopener noreferrer">hexa-mail.de</a></li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="footer-section">
|
<div class="footer-section">
|
||||||
|
|||||||
@@ -3,8 +3,6 @@
|
|||||||
* Helper functions for HexaHost.de
|
* Helper functions for HexaHost.de
|
||||||
*/
|
*/
|
||||||
|
|
||||||
require_once __DIR__ . '/../config/products-config.php';
|
|
||||||
|
|
||||||
// Sichere Session-Konfiguration
|
// Sichere Session-Konfiguration
|
||||||
if (session_status() === PHP_SESSION_NONE) {
|
if (session_status() === PHP_SESSION_NONE) {
|
||||||
// Session-Cookie-Sicherheit
|
// Session-Cookie-Sicherheit
|
||||||
|
|||||||
@@ -59,12 +59,12 @@
|
|||||||
<ul class="nav-menu">
|
<ul class="nav-menu">
|
||||||
<li><a href="/" class="nav-link <?php echo ($current_page === 'home') ? 'active' : ''; ?>">Home</a></li>
|
<li><a href="/" class="nav-link <?php echo ($current_page === 'home') ? 'active' : ''; ?>">Home</a></li>
|
||||||
<li class="nav-dropdown">
|
<li class="nav-dropdown">
|
||||||
<a href="#" class="nav-link <?php echo (in_array($current_page, getVisibleProductPageIds(), true)) ? 'active' : ''; ?>">Produkte</a>
|
<a href="#" class="nav-link <?php echo (in_array($current_page, ['vpc', 'vps', 'mail-gateway', 'webhosting'])) ? 'active' : ''; ?>">Produkte</a>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
<li<?php echo productHiddenAttr('vpc'); ?>><a href="/vpc" class="<?php echo ($current_page === 'vpc') ? 'active' : ''; ?>">Virtual Private Container</a></li>
|
<li><a href="/vpc" class="<?php echo ($current_page === 'vpc') ? 'active' : ''; ?>">Virtual Private Container</a></li>
|
||||||
<li<?php echo productHiddenAttr('vps'); ?>><a href="/vps" class="<?php echo ($current_page === 'vps') ? 'active' : ''; ?>">Virtual Private Server</a></li>
|
<li><a href="/vps" class="<?php echo ($current_page === 'vps') ? 'active' : ''; ?>">Virtual Private Server</a></li>
|
||||||
<li<?php echo productHiddenAttr('mail-gateway'); ?>><a href="/mail-gateway" class="<?php echo ($current_page === 'mail-gateway') ? 'active' : ''; ?>">Mail Gateway</a></li>
|
<li><a href="/mail-gateway" class="<?php echo ($current_page === 'mail-gateway') ? 'active' : ''; ?>">Mail Gateway</a></li>
|
||||||
<li<?php echo productHiddenAttr('webhosting'); ?>><a href="/webhosting" class="<?php echo ($current_page === 'webhosting') ? 'active' : ''; ?>">Webhosting</a></li>
|
<li><a href="/webhosting" class="<?php echo ($current_page === 'webhosting') ? 'active' : ''; ?>">Webhosting</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li><a href="/it-dienstleistungen" class="nav-link <?php echo ($current_page === 'it-dienstleistungen') ? 'active' : ''; ?>">IT-Dienstleistungen</a></li>
|
<li><a href="/it-dienstleistungen" class="nav-link <?php echo ($current_page === 'it-dienstleistungen') ? 'active' : ''; ?>">IT-Dienstleistungen</a></li>
|
||||||
|
|||||||
@@ -45,10 +45,8 @@
|
|||||||
.legal-section h3,
|
.legal-section h3,
|
||||||
.legal-block p,
|
.legal-block p,
|
||||||
.legal-block li,
|
.legal-block li,
|
||||||
.legal-hero .breadcrumb,
|
.breadcrumb,
|
||||||
.legal-hero .breadcrumb span,
|
.breadcrumb span {
|
||||||
.legal-content .breadcrumb,
|
|
||||||
.legal-content .breadcrumb span {
|
|
||||||
color: #000000;
|
color: #000000;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,14 +81,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.legal-block a,
|
.legal-block a,
|
||||||
.legal-hero .breadcrumb a,
|
.breadcrumb a {
|
||||||
.legal-content .breadcrumb a {
|
|
||||||
color: #0b57d0;
|
color: #0b57d0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.legal-block a:hover,
|
.legal-block a:hover,
|
||||||
.legal-hero .breadcrumb a:hover,
|
.breadcrumb a:hover {
|
||||||
.legal-content .breadcrumb a:hover {
|
|
||||||
color: #0b57d0;
|
color: #0b57d0;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ includeHeader($page_title, $page_description, $current_page);
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="products-grid">
|
<div class="products-grid">
|
||||||
<div class="product-card glass-card"<?php echo productHiddenAttr('vpc'); ?>>
|
<div class="product-card glass-card">
|
||||||
<div class="product-icon">
|
<div class="product-icon">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<path d="M4 7V4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v3"/>
|
<path d="M4 7V4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v3"/>
|
||||||
@@ -74,7 +74,7 @@ includeHeader($page_title, $page_description, $current_page);
|
|||||||
</ul>
|
</ul>
|
||||||
<a href="/vpc" class="btn btn-primary">Mehr erfahren</a>
|
<a href="/vpc" class="btn btn-primary">Mehr erfahren</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="product-card glass-card"<?php echo productHiddenAttr('vps'); ?>>
|
<div class="product-card glass-card">
|
||||||
<div class="product-icon">
|
<div class="product-icon">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"/>
|
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"/>
|
||||||
@@ -92,7 +92,7 @@ includeHeader($page_title, $page_description, $current_page);
|
|||||||
</ul>
|
</ul>
|
||||||
<a href="/vps" class="btn btn-primary">Mehr erfahren</a>
|
<a href="/vps" class="btn btn-primary">Mehr erfahren</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="product-card glass-card"<?php echo productHiddenAttr('mail-gateway'); ?>>
|
<div class="product-card glass-card">
|
||||||
<div class="product-icon">
|
<div class="product-icon">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/>
|
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/>
|
||||||
|
|||||||
Binary file not shown.
@@ -1,463 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import hashlib
|
|
||||||
import re
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import tempfile
|
|
||||||
from collections import defaultdict
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
TEXT_EXTENSIONS = {".php", ".html", ".htm", ".xml", ".txt", ".js", ".css"}
|
|
||||||
HASH_SUFFIX_RE = re.compile(r"\.[a-f0-9]{12}$", re.I)
|
|
||||||
|
|
||||||
|
|
||||||
def strip_comments_keep_strings(text: str) -> str:
|
|
||||||
out = []
|
|
||||||
i = 0
|
|
||||||
n = len(text)
|
|
||||||
in_single = False
|
|
||||||
in_double = False
|
|
||||||
in_template = False
|
|
||||||
escape = False
|
|
||||||
in_line_comment = False
|
|
||||||
in_block_comment = False
|
|
||||||
|
|
||||||
while i < n:
|
|
||||||
ch = text[i]
|
|
||||||
nxt = text[i + 1] if i + 1 < n else ""
|
|
||||||
|
|
||||||
if in_line_comment:
|
|
||||||
if ch == "\n":
|
|
||||||
in_line_comment = False
|
|
||||||
out.append(ch)
|
|
||||||
i += 1
|
|
||||||
continue
|
|
||||||
|
|
||||||
if in_block_comment:
|
|
||||||
if ch == "*" and nxt == "/":
|
|
||||||
in_block_comment = False
|
|
||||||
i += 2
|
|
||||||
else:
|
|
||||||
if ch == "\n":
|
|
||||||
out.append("\n")
|
|
||||||
i += 1
|
|
||||||
continue
|
|
||||||
|
|
||||||
if in_single or in_double or in_template:
|
|
||||||
out.append(ch)
|
|
||||||
if escape:
|
|
||||||
escape = False
|
|
||||||
elif ch == "\\":
|
|
||||||
escape = True
|
|
||||||
elif in_single and ch == "'":
|
|
||||||
in_single = False
|
|
||||||
elif in_double and ch == '"':
|
|
||||||
in_double = False
|
|
||||||
elif in_template and ch == "`":
|
|
||||||
in_template = False
|
|
||||||
i += 1
|
|
||||||
continue
|
|
||||||
|
|
||||||
if ch == "/" and nxt == "/":
|
|
||||||
in_line_comment = True
|
|
||||||
i += 2
|
|
||||||
continue
|
|
||||||
if ch == "/" and nxt == "*":
|
|
||||||
in_block_comment = True
|
|
||||||
i += 2
|
|
||||||
continue
|
|
||||||
|
|
||||||
if ch == "'":
|
|
||||||
in_single = True
|
|
||||||
out.append(ch)
|
|
||||||
i += 1
|
|
||||||
continue
|
|
||||||
if ch == '"':
|
|
||||||
in_double = True
|
|
||||||
out.append(ch)
|
|
||||||
i += 1
|
|
||||||
continue
|
|
||||||
if ch == "`":
|
|
||||||
in_template = True
|
|
||||||
out.append(ch)
|
|
||||||
i += 1
|
|
||||||
continue
|
|
||||||
|
|
||||||
out.append(ch)
|
|
||||||
i += 1
|
|
||||||
|
|
||||||
return "".join(out)
|
|
||||||
|
|
||||||
|
|
||||||
def strip_php_comments(text: str) -> str:
|
|
||||||
out = []
|
|
||||||
i = 0
|
|
||||||
n = len(text)
|
|
||||||
in_single = False
|
|
||||||
in_double = False
|
|
||||||
in_line_comment = False
|
|
||||||
in_block_comment = False
|
|
||||||
escape = False
|
|
||||||
|
|
||||||
while i < n:
|
|
||||||
ch = text[i]
|
|
||||||
nxt = text[i + 1] if i + 1 < n else ""
|
|
||||||
|
|
||||||
if in_line_comment:
|
|
||||||
if ch == "\n":
|
|
||||||
in_line_comment = False
|
|
||||||
out.append("\n")
|
|
||||||
i += 1
|
|
||||||
continue
|
|
||||||
|
|
||||||
if in_block_comment:
|
|
||||||
if ch == "*" and nxt == "/":
|
|
||||||
in_block_comment = False
|
|
||||||
i += 2
|
|
||||||
else:
|
|
||||||
if ch == "\n":
|
|
||||||
out.append("\n")
|
|
||||||
i += 1
|
|
||||||
continue
|
|
||||||
|
|
||||||
if in_single or in_double:
|
|
||||||
out.append(ch)
|
|
||||||
if escape:
|
|
||||||
escape = False
|
|
||||||
elif ch == "\\":
|
|
||||||
escape = True
|
|
||||||
elif in_single and ch == "'":
|
|
||||||
in_single = False
|
|
||||||
elif in_double and ch == '"':
|
|
||||||
in_double = False
|
|
||||||
i += 1
|
|
||||||
continue
|
|
||||||
|
|
||||||
if ch == "/" and nxt == "/":
|
|
||||||
in_line_comment = True
|
|
||||||
i += 2
|
|
||||||
continue
|
|
||||||
if ch == "#":
|
|
||||||
in_line_comment = True
|
|
||||||
i += 1
|
|
||||||
continue
|
|
||||||
if ch == "/" and nxt == "*":
|
|
||||||
in_block_comment = True
|
|
||||||
i += 2
|
|
||||||
continue
|
|
||||||
|
|
||||||
if ch == "'":
|
|
||||||
in_single = True
|
|
||||||
elif ch == '"':
|
|
||||||
in_double = True
|
|
||||||
|
|
||||||
out.append(ch)
|
|
||||||
i += 1
|
|
||||||
|
|
||||||
return "".join(out)
|
|
||||||
|
|
||||||
|
|
||||||
def minify_css_fallback(text: str) -> str:
|
|
||||||
text = strip_comments_keep_strings(text)
|
|
||||||
text = re.sub(r"\s+", " ", text)
|
|
||||||
text = re.sub(r"\s*([{}:;,>+~])\s*", r"\1", text)
|
|
||||||
return text.strip()
|
|
||||||
|
|
||||||
|
|
||||||
def minify_js_fallback(text: str) -> str:
|
|
||||||
text = strip_comments_keep_strings(text)
|
|
||||||
text = re.sub(r"\s+", " ", text)
|
|
||||||
text = re.sub(r"\s*([{}:;,()=+\-*/<>!&|?])\s*", r"\1", text)
|
|
||||||
return text.strip()
|
|
||||||
|
|
||||||
|
|
||||||
def canonical_asset_base(stem: str) -> str:
|
|
||||||
name = stem
|
|
||||||
while HASH_SUFFIX_RE.search(name):
|
|
||||||
name = HASH_SUFFIX_RE.sub("", name)
|
|
||||||
return name
|
|
||||||
|
|
||||||
|
|
||||||
def is_skipped_asset(path: Path) -> bool:
|
|
||||||
lowered = path.as_posix().lower()
|
|
||||||
if ".min." in path.name or ".obf." in path.name or ".deob." in path.name:
|
|
||||||
return True
|
|
||||||
if "deobfuscated" in lowered:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def is_valid_source_content(content: str) -> bool:
|
|
||||||
if "[javascript-obfuscator-cli]" in content:
|
|
||||||
return False
|
|
||||||
return len(content.strip()) >= 20
|
|
||||||
|
|
||||||
|
|
||||||
def collect_asset_groups(asset_root: Path) -> dict[tuple[Path, str, str], list[Path]]:
|
|
||||||
groups: dict[tuple[Path, str, str], list[Path]] = defaultdict(list)
|
|
||||||
for ext in (".js", ".css"):
|
|
||||||
for file_path in sorted(asset_root.rglob(f"*{ext}")):
|
|
||||||
if is_skipped_asset(file_path):
|
|
||||||
continue
|
|
||||||
base = canonical_asset_base(file_path.stem)
|
|
||||||
key = (file_path.parent, base, ext)
|
|
||||||
groups[key].append(file_path)
|
|
||||||
return groups
|
|
||||||
|
|
||||||
|
|
||||||
def pick_source_file(paths: list[Path], base: str, ext: str) -> Path | None:
|
|
||||||
if not paths:
|
|
||||||
return None
|
|
||||||
|
|
||||||
parent = paths[0].parent
|
|
||||||
plain = parent / f"{base}{ext}"
|
|
||||||
ordered: list[Path] = []
|
|
||||||
if plain in paths:
|
|
||||||
ordered.append(plain)
|
|
||||||
for candidate in sorted(paths, key=lambda p: len(p.name)):
|
|
||||||
if candidate not in ordered:
|
|
||||||
ordered.append(candidate)
|
|
||||||
|
|
||||||
for candidate in ordered:
|
|
||||||
try:
|
|
||||||
content = candidate.read_text(encoding="utf-8")
|
|
||||||
except (OSError, UnicodeDecodeError):
|
|
||||||
continue
|
|
||||||
if is_valid_source_content(content):
|
|
||||||
return candidate
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def cleanup_invalid_siblings(paths: list[Path], source: Path) -> None:
|
|
||||||
for path in paths:
|
|
||||||
if path == source or not path.exists():
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
content = path.read_text(encoding="utf-8")
|
|
||||||
except (OSError, UnicodeDecodeError):
|
|
||||||
content = ""
|
|
||||||
if not is_valid_source_content(content):
|
|
||||||
path.unlink()
|
|
||||||
|
|
||||||
|
|
||||||
def run_cmd(command: list[str], cwd: Path) -> None:
|
|
||||||
proc = subprocess.run(
|
|
||||||
command,
|
|
||||||
cwd=str(cwd),
|
|
||||||
text=True,
|
|
||||||
capture_output=True,
|
|
||||||
check=False,
|
|
||||||
)
|
|
||||||
if proc.returncode != 0:
|
|
||||||
raise RuntimeError(proc.stderr.strip() or proc.stdout.strip() or "command failed")
|
|
||||||
|
|
||||||
|
|
||||||
def process_js(path: Path, cwd: Path) -> None:
|
|
||||||
original = path.read_text(encoding="utf-8")
|
|
||||||
if not is_valid_source_content(original):
|
|
||||||
raise ValueError(
|
|
||||||
f"invalid or corrupted JS source: {path} "
|
|
||||||
f"(restore e.g. 'git checkout dev -- {path.as_posix()}')"
|
|
||||||
)
|
|
||||||
|
|
||||||
if shutil.which("npx"):
|
|
||||||
tmpdir = Path(tempfile.mkdtemp())
|
|
||||||
try:
|
|
||||||
src = tmpdir / "input.js"
|
|
||||||
out = tmpdir / "output.js"
|
|
||||||
src.write_text(original, encoding="utf-8")
|
|
||||||
run_cmd(
|
|
||||||
[
|
|
||||||
"npx",
|
|
||||||
"--yes",
|
|
||||||
"terser",
|
|
||||||
str(src),
|
|
||||||
"-o",
|
|
||||||
str(src),
|
|
||||||
"--compress",
|
|
||||||
"--mangle",
|
|
||||||
"--comments",
|
|
||||||
"false",
|
|
||||||
],
|
|
||||||
cwd,
|
|
||||||
)
|
|
||||||
run_cmd(
|
|
||||||
[
|
|
||||||
"npx",
|
|
||||||
"--yes",
|
|
||||||
"javascript-obfuscator",
|
|
||||||
str(src),
|
|
||||||
"--output",
|
|
||||||
str(out),
|
|
||||||
"--compact",
|
|
||||||
"true",
|
|
||||||
"--control-flow-flattening",
|
|
||||||
"true",
|
|
||||||
"--dead-code-injection",
|
|
||||||
"true",
|
|
||||||
"--string-array",
|
|
||||||
"true",
|
|
||||||
"--string-array-encoding",
|
|
||||||
"base64",
|
|
||||||
"--target",
|
|
||||||
"browser-no-eval",
|
|
||||||
"--source-map",
|
|
||||||
"false",
|
|
||||||
],
|
|
||||||
cwd,
|
|
||||||
)
|
|
||||||
if not out.exists():
|
|
||||||
raise RuntimeError("obfuscator produced no output file")
|
|
||||||
result = out.read_text(encoding="utf-8")
|
|
||||||
if not is_valid_source_content(result):
|
|
||||||
raise RuntimeError("obfuscator output looks invalid")
|
|
||||||
path.write_text(result.strip() + "\n", encoding="utf-8")
|
|
||||||
return
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
finally:
|
|
||||||
shutil.rmtree(tmpdir, ignore_errors=True)
|
|
||||||
|
|
||||||
path.write_text(minify_js_fallback(original) + "\n", encoding="utf-8")
|
|
||||||
|
|
||||||
|
|
||||||
def process_css(path: Path, cwd: Path) -> None:
|
|
||||||
original = path.read_text(encoding="utf-8")
|
|
||||||
if shutil.which("npx"):
|
|
||||||
tmpdir = Path(tempfile.mkdtemp())
|
|
||||||
try:
|
|
||||||
src = tmpdir / "input.css"
|
|
||||||
out = tmpdir / "output.css"
|
|
||||||
src.write_text(original, encoding="utf-8")
|
|
||||||
run_cmd(
|
|
||||||
[
|
|
||||||
"npx",
|
|
||||||
"--yes",
|
|
||||||
"clean-css-cli",
|
|
||||||
str(src),
|
|
||||||
"-o",
|
|
||||||
str(out),
|
|
||||||
"--skip-rebase",
|
|
||||||
"-O2",
|
|
||||||
],
|
|
||||||
cwd,
|
|
||||||
)
|
|
||||||
path.write_text(out.read_text(encoding="utf-8").strip() + "\n", encoding="utf-8")
|
|
||||||
return
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
finally:
|
|
||||||
shutil.rmtree(tmpdir, ignore_errors=True)
|
|
||||||
path.write_text(minify_css_fallback(original) + "\n", encoding="utf-8")
|
|
||||||
|
|
||||||
|
|
||||||
def process_php(path: Path) -> None:
|
|
||||||
original = path.read_text(encoding="utf-8")
|
|
||||||
stripped = strip_php_comments(original)
|
|
||||||
path.write_text(stripped, encoding="utf-8")
|
|
||||||
|
|
||||||
|
|
||||||
def hash_file(path: Path) -> str:
|
|
||||||
return hashlib.sha256(path.read_bytes()).hexdigest()[:12]
|
|
||||||
|
|
||||||
|
|
||||||
def replace_references(root: Path, mapping: dict[str, str]) -> None:
|
|
||||||
for candidate in root.rglob("*"):
|
|
||||||
if not candidate.is_file() or candidate.suffix.lower() not in TEXT_EXTENSIONS:
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
content = candidate.read_text(encoding="utf-8")
|
|
||||||
except UnicodeDecodeError:
|
|
||||||
continue
|
|
||||||
updated = content
|
|
||||||
for src, dst in sorted(mapping.items(), key=lambda item: len(item[0]), reverse=True):
|
|
||||||
updated = updated.replace(src, dst)
|
|
||||||
updated = updated.replace("/" + src, "/" + dst)
|
|
||||||
if updated != content:
|
|
||||||
candidate.write_text(updated, encoding="utf-8")
|
|
||||||
|
|
||||||
|
|
||||||
def build_hash_mapping(public_root: Path) -> dict[str, str]:
|
|
||||||
mapping: dict[str, str] = {}
|
|
||||||
asset_root = public_root / "assets"
|
|
||||||
if not asset_root.exists():
|
|
||||||
return mapping
|
|
||||||
|
|
||||||
groups = collect_asset_groups(asset_root)
|
|
||||||
for (parent, base, ext), paths in groups.items():
|
|
||||||
source = pick_source_file(paths, base, ext)
|
|
||||||
if source is None:
|
|
||||||
continue
|
|
||||||
digest = hash_file(source)
|
|
||||||
target = parent / f"{base}.{digest}{ext}"
|
|
||||||
rel_new = target.relative_to(public_root).as_posix()
|
|
||||||
|
|
||||||
for old in paths:
|
|
||||||
rel_old = old.relative_to(public_root).as_posix()
|
|
||||||
if rel_old != rel_new:
|
|
||||||
mapping[rel_old] = rel_new
|
|
||||||
|
|
||||||
if source != target:
|
|
||||||
if target.exists():
|
|
||||||
target.unlink()
|
|
||||||
source.replace(target)
|
|
||||||
|
|
||||||
for old in paths:
|
|
||||||
if old != target and old.exists():
|
|
||||||
old.unlink()
|
|
||||||
|
|
||||||
return mapping
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
|
||||||
parser = argparse.ArgumentParser(description="Release obfuscation build.")
|
|
||||||
parser.add_argument("--root", default=".", help="Repository root")
|
|
||||||
parser.add_argument("--hash-assets", action="store_true", help="Hash JS/CSS file names")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
repo_root = Path(args.root).resolve()
|
|
||||||
public_root = repo_root / "public"
|
|
||||||
|
|
||||||
if not public_root.exists():
|
|
||||||
print("public directory not found", file=sys.stderr)
|
|
||||||
return 1
|
|
||||||
|
|
||||||
asset_root = public_root / "assets"
|
|
||||||
if asset_root.exists():
|
|
||||||
groups = collect_asset_groups(asset_root)
|
|
||||||
for (parent, base, ext), paths in sorted(groups.items()):
|
|
||||||
source = pick_source_file(paths, base, ext)
|
|
||||||
if source is None:
|
|
||||||
rel = (parent / f"{base}{ext}").relative_to(public_root)
|
|
||||||
print(
|
|
||||||
f"ERROR: No valid source for {rel}. "
|
|
||||||
f"Restore from dev, e.g.: git checkout dev -- {rel}",
|
|
||||||
file=sys.stderr,
|
|
||||||
)
|
|
||||||
return 1
|
|
||||||
cleanup_invalid_siblings(paths, source)
|
|
||||||
if ext == ".js":
|
|
||||||
process_js(source, repo_root)
|
|
||||||
else:
|
|
||||||
process_css(source, repo_root)
|
|
||||||
|
|
||||||
for php in sorted(public_root.rglob("*.php")):
|
|
||||||
process_php(php)
|
|
||||||
for php in sorted((repo_root / "backend").rglob("*.php")):
|
|
||||||
process_php(php)
|
|
||||||
|
|
||||||
if args.hash_assets:
|
|
||||||
mapping = build_hash_mapping(public_root)
|
|
||||||
replace_references(repo_root, mapping)
|
|
||||||
|
|
||||||
print("Release obfuscation complete.")
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
raise SystemExit(main())
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
# Einmal pro Clone ausführen: Commit-Template + Conventional-Commits-Hook aktivieren
|
|
||||||
$ErrorActionPreference = "Stop"
|
|
||||||
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..")
|
|
||||||
|
|
||||||
Push-Location $repoRoot
|
|
||||||
try {
|
|
||||||
git config --local commit.template .gitmessage
|
|
||||||
git config --local core.hooksPath .githooks
|
|
||||||
|
|
||||||
Write-Host "Git Hooks aktiv:" -ForegroundColor Green
|
|
||||||
Write-Host " commit.template = .gitmessage"
|
|
||||||
Write-Host " core.hooksPath = .githooks"
|
|
||||||
Write-Host ""
|
|
||||||
Write-Host "Commit-Format: feat(scope): beschreibung"
|
|
||||||
} finally {
|
|
||||||
Pop-Location
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* HexaHost.de E-Mail Test (nur CLI oder lokale Entwicklung)
|
|
||||||
*/
|
|
||||||
|
|
||||||
if (PHP_SAPI !== 'cli') {
|
|
||||||
$remoteAddr = $_SERVER['REMOTE_ADDR'] ?? '';
|
|
||||||
$isLocal = in_array($remoteAddr, ['127.0.0.1', '::1'], true)
|
|
||||||
|| filter_var($remoteAddr, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false;
|
|
||||||
|
|
||||||
if (!$isLocal) {
|
|
||||||
http_response_code(403);
|
|
||||||
exit('Forbidden');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
require_once __DIR__ . '/../backend/config/mail-config.php';
|
|
||||||
|
|
||||||
function testEmail() {
|
|
||||||
$config = getHexaHostConfig();
|
|
||||||
|
|
||||||
$subject = '[HexaHost.de] Test-E-Mail';
|
|
||||||
$message = "Test-E-Mail von HexaHost.de\n\n";
|
|
||||||
$message .= "Zeitstempel: " . date('d.m.Y H:i:s') . "\n";
|
|
||||||
|
|
||||||
$headers = [
|
|
||||||
'From: ' . $config['from_name'] . ' <' . $config['from_email'] . '>',
|
|
||||||
'MIME-Version: 1.0',
|
|
||||||
'Content-Type: text/plain; charset=UTF-8',
|
|
||||||
'X-Mailer: HexaHost Test Email',
|
|
||||||
];
|
|
||||||
|
|
||||||
return mail($config['to_email'], $subject, $message, implode("\r\n", $headers));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (PHP_SAPI === 'cli') {
|
|
||||||
echo testEmail() ? "Test-E-Mail gesendet.\n" : "Fehler beim Senden.\n";
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($_GET['test'])) {
|
|
||||||
echo testEmail()
|
|
||||||
? 'Test-E-Mail wurde gesendet.'
|
|
||||||
: 'Fehler beim Senden der Test-E-Mail.';
|
|
||||||
} else {
|
|
||||||
echo '<h1>HexaHost.de E-Mail Test</h1>';
|
|
||||||
echo '<p><a href="?test=1">Test-E-Mail senden</a></p>';
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user