Compare commits

18 Commits

Author SHA1 Message Date
gitea-actions
e09fa1eab8 chore(release): obfuscate and hash production assets [skip ci] 2026-06-02 14:24:00 +00:00
gitea-actions
e9f804d967 chore(release): obfuscate and hash production assets [skip ci] 2026-06-02 14:01:36 +00:00
gitea-actions
3d54cea5ec chore(release): obfuscate and hash production assets [skip ci] 2026-06-01 06:50:07 +00:00
gitea-actions
fdd0367281 chore(release): obfuscate and hash production assets [skip ci] 2026-06-01 06:38:01 +00:00
gitea-actions
ded8778b6c chore(release): obfuscate and hash production assets [skip ci] 2026-05-29 09:04:26 +00:00
gitea-actions
7a03f5aa1b chore(release): obfuscate and hash production assets [skip ci] 2026-05-29 08:43:42 +00:00
gitea-actions
f7ea36f4f2 chore(release): obfuscate and hash production assets [skip ci] 2026-05-28 15:42:41 +00:00
gitea-actions
99f0056106 ci: merge dev for release build 2026-05-28 15:42:22 +00:00
smueller
e9d5b55459 Refactor release build process: Updated README.md to clarify branch usage and workflow for development and production. Enhanced Gitea and GitHub workflows to automate merging from dev to main, ensuring consistent obfuscation and asset management during releases. Improved commit messages and streamlined the build process for better clarity and efficiency.
All checks were successful
Release Build (dev → main) / release-build (push) Successful in 29s
2026-05-28 17:42:06 +02:00
smueller
8f985da61f Enhance obfuscation workflow and asset processing: Updated the Gitea workflow to skip CI for specific commit messages, improving efficiency. Refactored the obfuscation script to include better asset handling, validation, and cleanup processes, ensuring only valid files are processed. Introduced temporary directories for intermediate files during obfuscation, enhancing reliability and reducing errors. 2026-05-28 17:32:21 +02:00
gitea-actions
e91a9ed9c3 chore(release): obfuscate and hash production assets [skip ci] 2026-05-28 15:28:24 +00:00
smueller
76aceddcca fix(release): restore clean assets before obfuscation rebuild
All checks were successful
Obfuscate Main Build / obfuscate (push) Successful in 25s
2026-05-28 17:27:51 +02:00
smueller
d7851763f7 refactor(obfuscate_release): improve source file selection and cleanup logic, enhancing error handling for asset processing
Some checks failed
Obfuscate Main Build / obfuscate (push) Failing after 13s
2026-05-28 17:24:05 +02:00
smueller
1c0a3ff468 refactor(obfuscate_release): enhance asset processing and validation logic for JS and CSS files
Some checks failed
Obfuscate Main Build / obfuscate (push) Failing after 12s
2026-05-28 17:20:50 +02:00
smueller
6c9114e0a7 Update obfuscate workflow: Introduced environment variables for Gitea host and repository path, streamlined commit process to skip CI for bot commits, and adjusted remote URL configuration for secure pushes. Enhanced build process by removing unnecessary steps and ensuring efficient asset obfuscation and hashing. 2026-05-28 17:13:16 +02:00
gitea-actions
4b9940c18b chore(release): obfuscate and hash production assets [skip ci] 2026-05-28 15:12:33 +00:00
smueller
24a852aab5 Merge branch 'main' of https://git.hexahost.dev/smueller/HexaHost-Frontend
All checks were successful
Obfuscate Main Build / obfuscate (push) Successful in 25s
2026-05-28 17:12:00 +02:00
smueller
219f1d2fcf Update Gitea workflow for obfuscation: Added environment variables for Gitea host and repository path, modified git remote URL for pushing obfuscated builds, and ensured proper handling of commits with no changes. This enhances the deployment process for production assets. 2026-05-28 17:11:57 +02:00
30 changed files with 429 additions and 98 deletions

View File

@@ -1,29 +1,27 @@
name: Obfuscate Main Build name: Release Build (ci → main)
on: on:
push: push:
branches: branches:
- main - ci
workflow_dispatch: workflow_dispatch:
env:
GITEA_HOST: git.hexahost.dev
REPO_PATH: smueller/HexaHost-Frontend
jobs: jobs:
obfuscate: release-build:
if: ${{ !contains(github.event.head_commit.message, '[skip ci]') }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout ci (Integration)
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
repository-url: https://git.hexahost.dev/smueller/HexaHost-Frontend
- name: Skip loop commits ref: ci
run: |
msg="$(git log -1 --pretty=%B)"
echo "Last commit message: $msg"
if echo "$msg" | grep -q "\[skip ci\]"; then
echo "Skip CI commit detected."
exit 0
fi
- name: Setup Python - name: Setup Python
uses: actions/setup-python@v5 uses: actions/setup-python@v5
@@ -38,14 +36,27 @@ jobs:
- name: Run release obfuscation - name: Run release obfuscation
run: python scripts/obfuscate_release.py --root . --hash-assets run: python scripts/obfuscate_release.py --root . --hash-assets
- name: Commit obfuscated build - name: Publish release to main
env:
GITEA_TOKEN: ${{ github.token }}
run: | run: |
git config user.name "gitea-actions" git config user.name "gitea-actions"
git config user.email "actions@local" 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 git add -A
if git diff --cached --quiet; then if git diff --cached --quiet; then
echo "No build changes to commit." echo "No release changes to publish."
exit 0 exit 0
fi fi
git commit -m "chore(release): obfuscate and hash production assets [skip ci]"
git push 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
View 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

View File

@@ -1,44 +1,63 @@
name: Obfuscate Main Build # Hinweis: Gitea nutzt .gitea/workflows/obfuscate-main.yml (identischer Ablauf).
name: Release Build (ci → main)
on: on:
push: push:
branches: branches:
- main - ci
workflow_dispatch: workflow_dispatch:
permissions: env:
contents: write GITEA_HOST: git.hexahost.dev
REPO_PATH: smueller/HexaHost-Frontend
jobs: jobs:
obfuscate: release-build:
if: github.actor != 'github-actions[bot]' if: ${{ !contains(github.event.head_commit.message, '[skip ci]') }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout ci (Integration)
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
repository-url: https://git.hexahost.dev/smueller/HexaHost-Frontend
ref: ci
- name: Setup Python - name: Setup Python
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
python-version: '3.12' python-version: "3.12"
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: '20' node-version: "20"
- name: Run release obfuscation - name: Run release obfuscation
run: python scripts/obfuscate_release.py --root . --hash-assets run: python scripts/obfuscate_release.py --root . --hash-assets
- name: Commit obfuscated build - name: Publish release to main
env:
GITEA_TOKEN: ${{ github.token }}
run: | 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 git add -A
if git diff --cached --quiet; then if git diff --cached --quiet; then
echo "No build changes to commit." echo "No release changes to publish."
exit 0 exit 0
fi fi
git commit -m "chore(release): obfuscate and hash production assets [skip ci]"
git push 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
View File

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

View File

@@ -166,9 +166,38 @@ 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`. | Branch | Zweck |
|--------|--------|
| **`dev`** | Entwicklung (lesbarer Code, Kommentare) |
| **`ci`** | Integration (du mergst `dev` hierher) |
| **`main`** | Release/Produktion (obfuskiert, gehashte Assets — nur per Pipeline) |
Bei jedem Push/Merge auf `main` läuft die GitHub Action `.github/workflows/obfuscate-main.yml` automatisch und führt aus: **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
# Nach fertigen Änderungen auf dev:
git checkout ci
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 - Entfernen von Kommentaren (inkl. Block-Kommentaren) in PHP/JS/CSS
- Minify + Obfuscate für JavaScript - Minify + Obfuscate für JavaScript
@@ -176,7 +205,7 @@ Bei jedem Push/Merge auf `main` läuft die GitHub Action `.github/workflows/obfu
- Kein Source-Map-Output - Kein Source-Map-Output
- Hashing von JS/CSS-Dateinamen + automatische Referenz-Anpassung - Hashing von JS/CSS-Dateinamen + automatische Referenz-Anpassung
Lokal ausführbar: Lokal testen (nur in Kopie, nicht committen):
```bash ```bash
python scripts/obfuscate_release.py --root . --hash-assets python scripts/obfuscate_release.py --root . --hash-assets

View File

@@ -13,7 +13,7 @@ define('SMTP_TO_EMAIL', 'info@hexahost.de');
define('ENABLE_CSRF_PROTECTION', true); define('ENABLE_CSRF_PROTECTION', true);
define('ENABLE_RATE_LIMITING', true); define('ENABLE_RATE_LIMITING', true);
define('MAX_REQUESTS_PER_HOUR', 10); define('MAX_REQUESTS_PER_HOUR', 5);
define('ENABLE_SPAM_PROTECTION', true); define('ENABLE_SPAM_PROTECTION', true);

View File

@@ -444,12 +444,45 @@ $PRODUCTS['webhosting'] = [
]; ];
$PRODUCT_VISIBILITY = [
'vpc' => false,
'vps' => false,
'mail-gateway' => false,
'webhosting' => true,
];
function isProductVisible(string $productId): bool {
global $PRODUCT_VISIBILITY;
return $PRODUCT_VISIBILITY[$productId] ?? true;
}
function productHiddenAttr(string $productId): string {
return isProductVisible($productId) ? '' : ' hidden';
}
function getVisibleProductPageIds(): array {
global $PRODUCT_VISIBILITY;
return array_keys(array_filter($PRODUCT_VISIBILITY, static fn(bool $visible): bool => $visible));
}
function getAllProducts() { function getAllProducts() {
global $PRODUCTS; global $PRODUCTS;
return $PRODUCTS; return $PRODUCTS;

View File

@@ -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">
@@ -156,8 +163,8 @@
</script> </script>
<script async src="https://www.googletagmanager.com/gtag/js?id=G-EF0E9VPMTD"></script> <script async src="https://www.googletagmanager.com/gtag/js?id=G-EF0E9VPMTD"></script>
<script src="/assets/js/main.915e0206c30f.js" defer></script> <script src="/assets/js/main.e2f10a1bc052.js" defer></script>
<script src="/assets/js/cookie-consent.de9e404f0700.js" defer></script> <script src="/assets/js/cookie-consent.4c1a86945d56.js" defer></script>
<?php if (isset($additional_scripts)): ?> <?php if (isset($additional_scripts)): ?>
<?php foreach ($additional_scripts as $script): ?> <?php foreach ($additional_scripts as $script): ?>
<script src="<?php echo htmlspecialchars($script, ENT_QUOTES, 'UTF-8'); ?>" defer></script> <script src="<?php echo htmlspecialchars($script, ENT_QUOTES, 'UTF-8'); ?>" defer></script>

View File

@@ -3,6 +3,8 @@
require_once __DIR__ . '/../config/products-config.php';
if (session_status() === PHP_SESSION_NONE) { if (session_status() === PHP_SESSION_NONE) {

View File

@@ -33,7 +33,7 @@
<!-- Main Stylesheets --> <!-- Main Stylesheets -->
<link rel="stylesheet" href="/assets/css/style.d01979e8c871.css"> <link rel="stylesheet" href="/assets/css/style.d01979e8c871.css">
<link rel="stylesheet" href="/assets/css/custom.0bc6a878fec2.css"> <link rel="stylesheet" href="/assets/css/custom.9712c2869e20.css">
<!-- Fonts --> <!-- Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Russo+One&family=Source+Sans+Pro:wght@300;400;600;700&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Russo+One&family=Source+Sans+Pro:wght@300;400;600;700&display=swap" rel="stylesheet">
@@ -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>

View File

@@ -1 +0,0 @@
.btn-tertiary{color:var(--text-primary);background:transparent;border:1px solid rgba(255,255,255,0.25);transition:all 0.3s ease;}.btn-tertiary:hover{border-color:var(--primary-color);color:var(--primary-color);background:rgba(255,81,249,0.08);}.it-services-actions{justify-content:center;margin-top:2rem;}.legal-hero,.legal-content{background:#ffffff;color:#000000;}.legal-hero{margin-top:70px;padding:2rem 0 1.5rem;border-bottom:1px solid #e5e5e5;}.legal-content{padding-top:2rem;}.legal-hero-title{background:none;-webkit-text-fill-color:#000000;color:#000000;margin-bottom:0.5rem;}.legal-hero-description,.legal-section h2,.legal-section h3,.legal-block p,.legal-block li,.breadcrumb,.breadcrumb span{color:#000000;}.legal-section,.legal-section.glass-card{background:transparent;border:none;box-shadow:none;backdrop-filter:none;-webkit-backdrop-filter:none;border-radius:0;padding:0;}.legal-section:hover,.legal-section.glass-card:hover{transform:none;box-shadow:none;border:none;background:transparent;}.legal-content .glass-card:hover{transform:none;box-shadow:none;}.legal-section h2{border-bottom:1px solid #e5e5e5;padding-bottom:0.5rem;margin-bottom:0.8rem;}.legal-block a,.breadcrumb a{color:#0b57d0;}.legal-block a:hover,.breadcrumb a:hover{color:#0b57d0;text-decoration:none;}.legal-hero *,.legal-content *,.legal-hero *:hover,.legal-content *:hover,.legal-hero *:focus,.legal-content *:focus,.legal-hero *:active,.legal-content *:active{transform:none !important;box-shadow:none !important;text-shadow:none !important;transition:none !important;animation:none !important;}

View File

@@ -0,0 +1 @@
.btn-tertiary{color:var(--text-primary);background:transparent;border:1px solid rgba(255,255,255,0.25);transition:all 0.3s ease;}.btn-tertiary:hover{border-color:var(--primary-color);color:var(--primary-color);background:rgba(255,81,249,0.08);}.it-services-actions{justify-content:center;margin-top:2rem;}.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-hero,.legal-content{background:#ffffff;color:#000000;}.legal-hero{margin-top:70px;padding:2rem 0 1.5rem;border-bottom:1px solid #e5e5e5;}.legal-content{padding-top:2rem;}.legal-hero-title{background:none;-webkit-text-fill-color:#000000;color:#000000;margin-bottom:0.5rem;}.legal-hero-description,.legal-section h2,.legal-section h3,.legal-block p,.legal-block li,.legal-hero .breadcrumb,.legal-hero .breadcrumb span,.legal-content .breadcrumb,.legal-content .breadcrumb span{color:#000000;}.legal-section,.legal-section.glass-card{background:transparent;border:none;box-shadow:none;backdrop-filter:none;-webkit-backdrop-filter:none;border-radius:0;padding:0;}.legal-section:hover,.legal-section.glass-card:hover{transform:none;box-shadow:none;border:none;background:transparent;}.legal-content .glass-card:hover{transform:none;box-shadow:none;}.legal-section h2{border-bottom:1px solid #e5e5e5;padding-bottom:0.5rem;margin-bottom:0.8rem;}.legal-block a,.legal-hero .breadcrumb a,.legal-content .breadcrumb a{color:#0b57d0;}.legal-block a:hover,.legal-hero .breadcrumb a:hover,.legal-content .breadcrumb a:hover{color:#0b57d0;text-decoration:none;}.legal-hero *,.legal-content *,.legal-hero *:hover,.legal-content *:hover,.legal-hero *:focus,.legal-content *:focus,.legal-hero *:active,.legal-content *:active{transform:none !important;box-shadow:none !important;text-shadow:none !important;transition:none !important;animation:none !important;}.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

View File

@@ -1,5 +0,0 @@
[javascript-obfuscator-cli] Obfuscating file: public/assets/js/contact.b058cc66d435.js...
[javascript-obfuscator-cli] Obfuscating file: public/assets/js/cookie-consent.de9e404f0700.js...
[javascript-obfuscator-cli] Obfuscating file: public/assets/js/main.915e0206c30f.js...

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

File diff suppressed because one or more lines are too long

View File

@@ -8,7 +8,7 @@ $preselected_subject = getPreselectedContactSubject();
$page_title = 'Kontakt - HexaHost.de | Hosting aus Niederbayern'; $page_title = 'Kontakt - HexaHost.de | Hosting aus Niederbayern';
$page_description = 'Kontaktieren Sie HexaHost.de - Ihr Hosting-Partner aus Niederbayern. Persönlicher Support und kompetente Beratung.'; $page_description = 'Kontaktieren Sie HexaHost.de - Ihr Hosting-Partner aus Niederbayern. Persönlicher Support und kompetente Beratung.';
$current_page = 'contact'; $current_page = 'contact';
$additional_scripts = ['assets/js/contact.b058cc66d435.js']; $additional_scripts = ['assets/js/contact.641876ea7594.js'];
includeHeader($page_title, $page_description, $current_page, $additional_scripts); includeHeader($page_title, $page_description, $current_page, $additional_scripts);

View File

@@ -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"/>

View File

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

View File

@@ -7,8 +7,8 @@ Disallow: /assets/css/
# Allow CSS and JS files for better SEO # Allow CSS and JS files for better SEO
Allow: /assets/css/style.d01979e8c871.css Allow: /assets/css/style.d01979e8c871.css
Allow: /assets/js/main.915e0206c30f.js Allow: /assets/js/main.e2f10a1bc052.js
Allow: /assets/js/contact.b058cc66d435.js Allow: /assets/js/contact.641876ea7594.js
# Sitemap location # Sitemap location
Sitemap: https://hexahost.de/sitemap.xml Sitemap: https://hexahost.de/sitemap.xml

View File

@@ -3,15 +3,17 @@ from __future__ import annotations
import argparse import argparse
import hashlib import hashlib
import os
import re import re
import shutil import shutil
import subprocess import subprocess
import sys import sys
import tempfile
from collections import defaultdict
from pathlib import Path from pathlib import Path
TEXT_EXTENSIONS = {".php", ".html", ".htm", ".xml", ".txt", ".js", ".css"} 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: def strip_comments_keep_strings(text: str) -> str:
@@ -174,42 +176,124 @@ def minify_js_fallback(text: str) -> str:
return text.strip() return text.strip()
def run_cmd(command: list[str], cwd: Path, input_text: str | None = None) -> str: 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( proc = subprocess.run(
command, command,
cwd=str(cwd), cwd=str(cwd),
input=input_text,
text=True, text=True,
capture_output=True, capture_output=True,
check=False, check=False,
) )
if proc.returncode != 0: if proc.returncode != 0:
raise RuntimeError(proc.stderr.strip() or "command failed") raise RuntimeError(proc.stderr.strip() or proc.stdout.strip() or "command failed")
return proc.stdout
def process_js(path: Path, cwd: Path) -> None: def process_js(path: Path, cwd: Path) -> None:
original = path.read_text(encoding="utf-8") 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"): if shutil.which("npx"):
tmpdir = Path(tempfile.mkdtemp())
try: try:
minified = run_cmd( src = tmpdir / "input.js"
out = tmpdir / "output.js"
src.write_text(original, encoding="utf-8")
run_cmd(
[ [
"npx", "npx",
"--yes", "--yes",
"terser", "terser",
str(src),
"-o",
str(src),
"--compress", "--compress",
"--mangle", "--mangle",
"--comments", "--comments",
"false", "false",
], ],
cwd, cwd,
input_text=original,
) )
obfuscated = run_cmd( run_cmd(
[ [
"npx", "npx",
"--yes", "--yes",
"javascript-obfuscator", "javascript-obfuscator",
str(src),
"--output",
str(out),
"--compact", "--compact",
"true", "true",
"--control-flow-flattening", "--control-flow-flattening",
@@ -224,38 +308,51 @@ def process_js(path: Path, cwd: Path) -> None:
"browser-no-eval", "browser-no-eval",
"--source-map", "--source-map",
"false", "false",
"--output",
"stdout",
], ],
cwd, cwd,
input_text=minified,
) )
path.write_text(obfuscated.strip() + "\n", encoding="utf-8") 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 return
except Exception: except Exception:
pass pass
finally:
shutil.rmtree(tmpdir, ignore_errors=True)
path.write_text(minify_js_fallback(original) + "\n", encoding="utf-8") path.write_text(minify_js_fallback(original) + "\n", encoding="utf-8")
def process_css(path: Path, cwd: Path) -> None: def process_css(path: Path, cwd: Path) -> None:
original = path.read_text(encoding="utf-8") original = path.read_text(encoding="utf-8")
if shutil.which("npx"): if shutil.which("npx"):
tmpdir = Path(tempfile.mkdtemp())
try: try:
minified = run_cmd( src = tmpdir / "input.css"
out = tmpdir / "output.css"
src.write_text(original, encoding="utf-8")
run_cmd(
[ [
"npx", "npx",
"--yes", "--yes",
"clean-css-cli", "clean-css-cli",
str(src),
"-o",
str(out),
"--skip-rebase", "--skip-rebase",
"-O2", "-O2",
], ],
cwd, cwd,
input_text=original,
) )
path.write_text(minified.strip() + "\n", encoding="utf-8") path.write_text(out.read_text(encoding="utf-8").strip() + "\n", encoding="utf-8")
return return
except Exception: except Exception:
pass pass
finally:
shutil.rmtree(tmpdir, ignore_errors=True)
path.write_text(minify_css_fallback(original) + "\n", encoding="utf-8") path.write_text(minify_css_fallback(original) + "\n", encoding="utf-8")
@@ -266,8 +363,7 @@ def process_php(path: Path) -> None:
def hash_file(path: Path) -> str: def hash_file(path: Path) -> str:
digest = hashlib.sha256(path.read_bytes()).hexdigest()[:12] return hashlib.sha256(path.read_bytes()).hexdigest()[:12]
return digest
def replace_references(root: Path, mapping: dict[str, str]) -> None: def replace_references(root: Path, mapping: dict[str, str]) -> None:
@@ -279,7 +375,7 @@ def replace_references(root: Path, mapping: dict[str, str]) -> None:
except UnicodeDecodeError: except UnicodeDecodeError:
continue continue
updated = content updated = content
for src, dst in mapping.items(): 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)
updated = updated.replace("/" + src, "/" + dst) updated = updated.replace("/" + src, "/" + dst)
if updated != content: if updated != content:
@@ -289,17 +385,32 @@ def replace_references(root: Path, mapping: dict[str, str]) -> None:
def build_hash_mapping(public_root: Path) -> dict[str, str]: def build_hash_mapping(public_root: Path) -> dict[str, str]:
mapping: dict[str, str] = {} mapping: dict[str, str] = {}
asset_root = public_root / "assets" asset_root = public_root / "assets"
for ext in (".js", ".css"): if not asset_root.exists():
for file_path in sorted(asset_root.rglob(f"*{ext}")): return mapping
if ".min." in file_path.name or ".obf." in file_path.name:
continue groups = collect_asset_groups(asset_root)
digest = hash_file(file_path) for (parent, base, ext), paths in groups.items():
new_name = f"{file_path.stem}.{digest}{file_path.suffix}" source = pick_source_file(paths, base, ext)
new_path = file_path.with_name(new_name) if source is None:
file_path.rename(new_path) continue
rel_old = file_path.relative_to(public_root).as_posix() digest = hash_file(source)
rel_new = new_path.relative_to(public_root).as_posix() target = parent / f"{base}.{digest}{ext}"
mapping[rel_old] = rel_new 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 return mapping
@@ -316,11 +427,26 @@ def main() -> int:
print("public directory not found", file=sys.stderr) print("public directory not found", file=sys.stderr)
return 1 return 1
for js in sorted(public_root.rglob("*.js")): asset_root = public_root / "assets"
process_js(js, repo_root) if asset_root.exists():
for css in sorted(public_root.rglob("*.css")): groups = collect_asset_groups(asset_root)
process_css(css, repo_root) for (parent, base, ext), paths in sorted(groups.items()):
for php in sorted((repo_root / "public").rglob("*.php")): 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) process_php(php)
for php in sorted((repo_root / "backend").rglob("*.php")): for php in sorted((repo_root / "backend").rglob("*.php")):
process_php(php) process_php(php)

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

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