mirror of
https://git.hexahost.dev/smueller/HexaHost-Frontend.git
synced 2026-06-02 17:08:43 +00:00
Compare commits
14 Commits
b113bdeaa2
...
v1.4.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
56f3f90d95 | ||
|
|
c6b483ca25 | ||
|
|
0df5dc9b57 | ||
|
|
9c92df4ae4 | ||
|
|
e0bcf15121 | ||
|
|
1d4b751316 | ||
|
|
186b5ae199 | ||
|
|
bbc3cbae4e | ||
|
|
e9d5b55459 | ||
|
|
8f985da61f | ||
|
|
6c9114e0a7 | ||
|
|
f097da7eb1 | ||
|
|
b4b1dde484 | ||
|
|
481d223747 |
62
.gitea/workflows/obfuscate-main.yml
Normal file
62
.gitea/workflows/obfuscate-main.yml
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
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"
|
||||||
29
.githooks/commit-msg
Normal file
29
.githooks/commit-msg
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
#!/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
Normal file
63
.github/workflows/obfuscate-main.yml
vendored
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
# 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,6 +13,7 @@ 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
Normal file
16
.gitmessage
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# 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,25 +166,51 @@ Für den Produktivbetrieb `public/` als Webroot konfigurieren.
|
|||||||
|
|
||||||
### Production-Build & Veröffentlichung
|
### Production-Build & Veröffentlichung
|
||||||
|
|
||||||
Der Quellcode bleibt auf `dev`, der veröffentlichte Stand liegt auf `main` (ohne Kommentare, obfuskiertes JS).
|
| Branch | Zweck |
|
||||||
|
|--------|--------|
|
||||||
|
| **`dev`** | Entwicklung (lesbarer Code, Kommentare) |
|
||||||
|
| **`ci`** | Integration (du mergst `dev` hierher) |
|
||||||
|
| **`main`** | Release/Produktion (obfuskiert, gehashte Assets — nur per Pipeline) |
|
||||||
|
|
||||||
**Voraussetzungen:** Node.js 18+ (inkl. npm), PHP 8+ CLI, Git
|
**Ablauf: `dev` → `ci` → `main`**
|
||||||
|
|
||||||
|
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
|
||||||
# Windows
|
# Nach fertigen Änderungen auf dev:
|
||||||
.\scripts\run-build.ps1
|
git checkout ci
|
||||||
.\scripts\publish-to-main.ps1 -Push
|
git pull origin ci
|
||||||
|
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
|
||||||
# Linux / macOS
|
python scripts/obfuscate_release.py --root . --hash-assets
|
||||||
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', 10); // Max. Anfragen pro Stunde
|
define('MAX_REQUESTS_PER_HOUR', 5); // 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,10 +443,43 @@ $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,10 +15,17 @@
|
|||||||
<div class="footer-section">
|
<div class="footer-section">
|
||||||
<h4>Produkte</h4>
|
<h4>Produkte</h4>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="/vpc">Virtual Private Container</a></li>
|
<li<?php echo productHiddenAttr('vpc'); ?>><a href="/vpc">Virtual Private Container</a></li>
|
||||||
<li><a href="/vps">Virtual Private Server</a></li>
|
<li<?php echo productHiddenAttr('vps'); ?>><a href="/vps">Virtual Private Server</a></li>
|
||||||
<li><a href="/mail-gateway">Mail Gateway</a></li>
|
<li<?php echo productHiddenAttr('mail-gateway'); ?>><a href="/mail-gateway">Mail Gateway</a></li>
|
||||||
<li><a href="/webhosting">Webhosting</a></li>
|
<li<?php echo productHiddenAttr('webhosting'); ?>><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,6 +3,8 @@
|
|||||||
* 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, ['vpc', 'vps', 'mail-gateway', 'webhosting'])) ? 'active' : ''; ?>">Produkte</a>
|
<a href="#" class="nav-link <?php echo (in_array($current_page, getVisibleProductPageIds(), true)) ? 'active' : ''; ?>">Produkte</a>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
<li><a href="/vpc" class="<?php echo ($current_page === 'vpc') ? 'active' : ''; ?>">Virtual Private Container</a></li>
|
<li<?php echo productHiddenAttr('vpc'); ?>><a href="/vpc" class="<?php echo ($current_page === 'vpc') ? 'active' : ''; ?>">Virtual Private Container</a></li>
|
||||||
<li><a href="/vps" class="<?php echo ($current_page === 'vps') ? 'active' : ''; ?>">Virtual Private Server</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="/mail-gateway" class="<?php echo ($current_page === 'mail-gateway') ? 'active' : ''; ?>">Mail Gateway</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="/webhosting" class="<?php echo ($current_page === 'webhosting') ? 'active' : ''; ?>">Webhosting</a></li>
|
<li<?php echo productHiddenAttr('webhosting'); ?>><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>
|
||||||
|
|||||||
@@ -16,6 +16,26 @@
|
|||||||
margin-top: 2rem;
|
margin-top: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* IT services page: center target-group cards without affecting other pages */
|
||||||
|
.it-services-page .values .values-grid {
|
||||||
|
display: flex !important;
|
||||||
|
flex-wrap: wrap !important;
|
||||||
|
justify-content: center !important;
|
||||||
|
align-items: stretch;
|
||||||
|
grid-template-columns: none !important;
|
||||||
|
gap: 1.5rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.it-services-page .values .value-item {
|
||||||
|
flex: 0 1 320px !important;
|
||||||
|
max-width: 360px;
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
margin: 0 !important;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
/* Legal pages: plain white content with black text */
|
/* Legal pages: plain white content with black text */
|
||||||
.legal-hero,
|
.legal-hero,
|
||||||
.legal-content {
|
.legal-content {
|
||||||
@@ -45,8 +65,10 @@
|
|||||||
.legal-section h3,
|
.legal-section h3,
|
||||||
.legal-block p,
|
.legal-block p,
|
||||||
.legal-block li,
|
.legal-block li,
|
||||||
.breadcrumb,
|
.legal-hero .breadcrumb,
|
||||||
.breadcrumb span {
|
.legal-hero .breadcrumb span,
|
||||||
|
.legal-content .breadcrumb,
|
||||||
|
.legal-content .breadcrumb span {
|
||||||
color: #000000;
|
color: #000000;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,12 +103,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.legal-block a,
|
.legal-block a,
|
||||||
.breadcrumb a {
|
.legal-hero .breadcrumb a,
|
||||||
|
.legal-content .breadcrumb a {
|
||||||
color: #0b57d0;
|
color: #0b57d0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.legal-block a:hover,
|
.legal-block a:hover,
|
||||||
.breadcrumb a:hover {
|
.legal-hero .breadcrumb a:hover,
|
||||||
|
.legal-content .breadcrumb a:hover {
|
||||||
color: #0b57d0;
|
color: #0b57d0;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
@@ -106,3 +130,41 @@
|
|||||||
transition: none !important;
|
transition: none !important;
|
||||||
animation: none !important;
|
animation: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Keep footer sections in one row on desktop */
|
||||||
|
.footer-content {
|
||||||
|
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||||
|
gap: 1.5rem 1.25rem;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-section h4 {
|
||||||
|
margin-bottom: 0.65rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-section ul li {
|
||||||
|
margin-bottom: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1100px) {
|
||||||
|
.footer-content {
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.footer-content {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 520px) {
|
||||||
|
.footer-content {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.it-services-page .values .value-item {
|
||||||
|
flex-basis: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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">
|
<div class="product-card glass-card"<?php echo productHiddenAttr('vpc'); ?>>
|
||||||
<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">
|
<div class="product-card glass-card"<?php echo productHiddenAttr('vps'); ?>>
|
||||||
<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">
|
<div class="product-card glass-card"<?php echo productHiddenAttr('mail-gateway'); ?>>
|
||||||
<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"/>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ $current_page = 'it-dienstleistungen';
|
|||||||
includeHeader($page_title, $page_description, $current_page);
|
includeHeader($page_title, $page_description, $current_page);
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<main id="main-content">
|
<main id="main-content" class="it-services-page">
|
||||||
<!-- Services Hero -->
|
<!-- Services Hero -->
|
||||||
<section class="about-hero">
|
<section class="about-hero">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
|||||||
@@ -2,43 +2,43 @@
|
|||||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||||
<url>
|
<url>
|
||||||
<loc>https://hexahost.de/</loc>
|
<loc>https://hexahost.de/</loc>
|
||||||
<lastmod>2024-01-01</lastmod>
|
<lastmod>2026-05-28</lastmod>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>1.0</priority>
|
<priority>1.0</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://hexahost.de/vpc.html</loc>
|
<loc>https://hexahost.de/vpc.html</loc>
|
||||||
<lastmod>2024-01-01</lastmod>
|
<lastmod>2026-05-28</lastmod>
|
||||||
<changefreq>monthly</changefreq>
|
<changefreq>monthly</changefreq>
|
||||||
<priority>0.9</priority>
|
<priority>0.9</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://hexahost.de/vps.html</loc>
|
<loc>https://hexahost.de/vps.html</loc>
|
||||||
<lastmod>2024-01-01</lastmod>
|
<lastmod>2026-05-28</lastmod>
|
||||||
<changefreq>monthly</changefreq>
|
<changefreq>monthly</changefreq>
|
||||||
<priority>0.9</priority>
|
<priority>0.9</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://hexahost.de/mail-gateway.html</loc>
|
<loc>https://hexahost.de/mail-gateway.html</loc>
|
||||||
<lastmod>2024-01-01</lastmod>
|
<lastmod>2026-05-28</lastmod>
|
||||||
<changefreq>monthly</changefreq>
|
<changefreq>monthly</changefreq>
|
||||||
<priority>0.9</priority>
|
<priority>0.9</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://hexahost.de/webhosting.html</loc>
|
<loc>https://hexahost.de/webhosting.html</loc>
|
||||||
<lastmod>2024-01-01</lastmod>
|
<lastmod>2026-05-28</lastmod>
|
||||||
<changefreq>monthly</changefreq>
|
<changefreq>monthly</changefreq>
|
||||||
<priority>0.9</priority>
|
<priority>0.9</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://hexahost.de/about.html</loc>
|
<loc>https://hexahost.de/about.html</loc>
|
||||||
<lastmod>2024-01-01</lastmod>
|
<lastmod>2026-05-28</lastmod>
|
||||||
<changefreq>monthly</changefreq>
|
<changefreq>monthly</changefreq>
|
||||||
<priority>0.7</priority>
|
<priority>0.7</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://hexahost.de/contact.html</loc>
|
<loc>https://hexahost.de/contact.html</loc>
|
||||||
<lastmod>2024-01-01</lastmod>
|
<lastmod>2026-05-28</lastmod>
|
||||||
<changefreq>monthly</changefreq>
|
<changefreq>monthly</changefreq>
|
||||||
<priority>0.8</priority>
|
<priority>0.8</priority>
|
||||||
</url>
|
</url>
|
||||||
|
|||||||
BIN
scripts/__pycache__/obfuscate_release.cpython-314.pyc
Normal file
BIN
scripts/__pycache__/obfuscate_release.cpython-314.pyc
Normal file
Binary file not shown.
463
scripts/obfuscate_release.py
Normal file
463
scripts/obfuscate_release.py
Normal file
@@ -0,0 +1,463 @@
|
|||||||
|
#!/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())
|
||||||
17
scripts/setup-git-hooks.ps1
Normal file
17
scripts/setup-git-hooks.ps1
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# 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
|
||||||
|
}
|
||||||
48
scripts/test-email.php
Normal file
48
scripts/test-email.php
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<?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