fix: QC-Ratings aus 7 älteren Artikeln entfernt (CHECK 10)
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
# BSN Wertungsbox v2 — Joomla System Plugin
|
||||
|
||||
## Artikel-Typen
|
||||
|
||||
| Typ | Infobox | Wertungsbox |
|
||||
|---|---|---|
|
||||
| **News** | ❌ | ❌ |
|
||||
| **Review** | ✅ `{bsn_infobox}` | ✅ automatisch |
|
||||
| **Vorschau** | ✅ `{bsn_infobox}` | ❌ |
|
||||
|
||||
## Review-Artikel-Aufbau
|
||||
|
||||
```
|
||||
[Einleitung — Daniel schreibt]
|
||||
{bsn_infobox} ← Shortcode setzen!
|
||||
[Haupttext]
|
||||
← Wertungsbox erscheint HIER automatisch
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
1. **ZIP hochladen:** Administrator → System → Erweiterungen → Installieren
|
||||
2. **Plugin aktivieren:** System → Plugins → "System - BSN Wertungsbox" → Aktivieren
|
||||
3. **Kategorien wählen:** Plugin-Einstellungen → "Kategorien für Wertungsbox" → Nur die Review-Kategorie(n) anhaken
|
||||
4. Speichern
|
||||
|
||||
→ 19 Custom Fields sind automatisch angelegt in der Gruppe **"BSN Wertungsbox"**.
|
||||
|
||||
---
|
||||
|
||||
## Verwendung
|
||||
|
||||
### 1. Custom Fields ausfüllen (Tab "Felder" im Artikel-Editor)
|
||||
|
||||
**Infobox-Felder:**
|
||||
| Feld | Beispiel |
|
||||
|---|---|
|
||||
| BGG-Spiel-ID | 349463 |
|
||||
| Verlag | alea |
|
||||
| Designer | Richard Garfield |
|
||||
| Autor | — (falls abweichend) |
|
||||
| Illustrator | — |
|
||||
| Spieleranzahl | 1–4 |
|
||||
| Alter | ab 12 Jahren |
|
||||
| Spieldauer | 45–60 Minuten |
|
||||
| BGG-Rating | 7,0/10 |
|
||||
| BGG-Schwierigkeit | 1,89/5 |
|
||||
|
||||
**Wertungsbox-Felder:**
|
||||
| Feld | Beispiel |
|
||||
|---|---|
|
||||
| Wertung – Titel | Chrono Fall |
|
||||
| Wertung – Untertitel | At the End of Space and Time |
|
||||
| Wertung – Komplexität | Kennerspiel |
|
||||
| Wertung – Sprache | Deutsch |
|
||||
| Wertung – Mechanismen | Cooperative Game *(eine pro Zeile)** |
|
||||
| Wertung – Pro | hohe strategische Tiefe *(eine pro Zeile)** |
|
||||
| Wertung – Kontra | anspruchsvolle Einstiegshürde *(eine pro Zeile)** |
|
||||
| Wertung – Rezensent | Michael Berndt |
|
||||
| Wertung – Punkte | 84 |
|
||||
|
||||
### 2. Shortcode im Artikel platzieren
|
||||
|
||||
An die Stelle, wo die Infobox erscheinen soll:
|
||||
```
|
||||
{bsn_infobox}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Rückgängig machen
|
||||
|
||||
- **Plugin deaktivieren** → Boxen verschwinden sofort. Custom Fields + Werte bleiben erhalten.
|
||||
- **Plugin deinstallieren** → Custom Fields + Werte werden gelöscht.
|
||||
- **Artikel-Inhalte werden NIE verändert.** Der Shortcode `{bsn_infobox}` bleibt als Text im Artikel, falls das Plugin deaktiviert wird (einfach löschen).
|
||||
|
||||
---
|
||||
|
||||
## Sicherheit
|
||||
|
||||
- Läuft nur im Frontend (nicht im Admin)
|
||||
- Nur bei `text/html`-Output
|
||||
- Alle Operationen in `try/catch` → macht die Seite nie kaputt
|
||||
- Keine Datenbank-Schreibzugriffe beim Rendern (nur lesend)
|
||||
@@ -0,0 +1,515 @@
|
||||
<?php
|
||||
/**
|
||||
* @package BSN_Wertungsbox
|
||||
* @copyright (C) 2026 brettspiel-news.de
|
||||
* @license GPL-2.0-or-later
|
||||
*
|
||||
* System-Plugin: Rendert Infobox + Wertungsbox aus Joomla Custom Fields.
|
||||
* Astroid-kompatibel via onAfterRender. Kategorie-gesteuert.
|
||||
*
|
||||
* REVIEW-ARTIKEL-AUFBAU:
|
||||
* [Einleitung]
|
||||
* {bsn_infobox} ← Shortcode, manuell gesetzt
|
||||
* [Haupttext]
|
||||
* ← Wertungsbox erscheint automatisch hier
|
||||
*/
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Plugin\CMSPlugin;
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\Database\DatabaseInterface;
|
||||
use Joomla\CMS\Uri\Uri;
|
||||
|
||||
class PlgSystemBsn_Wertungsbox extends CMSPlugin
|
||||
{
|
||||
protected $app;
|
||||
protected $autoloadLanguage = true;
|
||||
|
||||
// Custom Field definitions
|
||||
private const FIELDS = [
|
||||
// Infobox
|
||||
'bsn-spiel-id' => ['label' => 'BGG-Spiel-ID', 'type' => 'text', 'group' => 'infobox'],
|
||||
'bsn-verlag' => ['label' => 'Verlag', 'type' => 'text', 'group' => 'infobox'],
|
||||
'bsn-designer' => ['label' => 'Designer', 'type' => 'text', 'group' => 'infobox'],
|
||||
'bsn-autor' => ['label' => 'Autor', 'type' => 'text', 'group' => 'infobox'],
|
||||
'bsn-illustrator' => ['label' => 'Illustrator', 'type' => 'text', 'group' => 'infobox'],
|
||||
'bsn-spieler' => ['label' => 'Spieleranzahl', 'type' => 'text', 'group' => 'infobox'],
|
||||
'bsn-alter' => ['label' => 'Alter', 'type' => 'text', 'group' => 'infobox'],
|
||||
'bsn-dauer' => ['label' => 'Spieldauer', 'type' => 'text', 'group' => 'infobox'],
|
||||
'bsn-bgg-rating' => ['label' => 'BGG-Rating', 'type' => 'text', 'group' => 'infobox'],
|
||||
'bsn-bgg-weight' => ['label' => 'BGG-Schwierigkeit', 'type' => 'text', 'group' => 'infobox'],
|
||||
// Wertungsbox
|
||||
'bsn-wertung-titel' => ['label' => 'Wertung – Titel', 'type' => 'text', 'group' => 'wertung'],
|
||||
'bsn-wertung-untertitel' => ['label' => 'Wertung – Untertitel', 'type' => 'text', 'group' => 'wertung'],
|
||||
'bsn-wertung-komplex' => ['label' => 'Wertung – Komplexität', 'type' => 'text', 'group' => 'wertung'],
|
||||
'bsn-wertung-sprache' => ['label' => 'Wertung – Sprache', 'type' => 'text', 'group' => 'wertung'],
|
||||
'bsn-wertung-mechanik' => ['label' => 'Wertung – Mechanismen', 'type' => 'textarea', 'group' => 'wertung'],
|
||||
'bsn-wertung-pro' => ['label' => 'Wertung – Pro', 'type' => 'textarea', 'group' => 'wertung'],
|
||||
'bsn-wertung-kontra' => ['label' => 'Wertung – Kontra', 'type' => 'textarea', 'group' => 'wertung'],
|
||||
'bsn-wertung-autor' => ['label' => 'Wertung – Rezensent', 'type' => 'text', 'group' => 'wertung'],
|
||||
'bsn-wertung-punkte' => ['label' => 'Wertung – Punkte', 'type' => 'integer', 'group' => 'wertung'],
|
||||
];
|
||||
|
||||
/**
|
||||
* onAfterRender — the Astroid-safe injection point
|
||||
*/
|
||||
public function onAfterRender(): void
|
||||
{
|
||||
try {
|
||||
// Safety: only frontend, only HTML
|
||||
if ($this->app->isClient('administrator')) return;
|
||||
|
||||
$body = $this->app->getBody();
|
||||
$contentType = $this->app->getDocument()->getType();
|
||||
if ($contentType !== 'html') return;
|
||||
|
||||
// Only process if we're on an article page (heuristic: com_content article container)
|
||||
$hasArticle = preg_match('/com-content-article.*itemscope/', $body);
|
||||
if (!$hasArticle) return;
|
||||
|
||||
// 1) Replace {bsn_infobox} shortcodes
|
||||
$body = $this->processInfoboxShortcodes($body);
|
||||
|
||||
// 2) Inject wertungsbox if category matches
|
||||
$body = $this->processWertungsbox($body);
|
||||
|
||||
$this->app->setBody($body);
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
// Silent fail — never break the site
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find and replace {bsn_infobox} shortcodes with styled HTML
|
||||
*/
|
||||
private function processInfoboxShortcodes(string $body): string
|
||||
{
|
||||
if (!preg_match_all('/\{bsn_infobox\}/', $body, $matches, PREG_OFFSET_CAPTURE)) {
|
||||
return $body;
|
||||
}
|
||||
|
||||
// Find article IDs for each shortcode occurrence
|
||||
$replacements = [];
|
||||
foreach ($matches[0] as $match) {
|
||||
$pos = $match[1];
|
||||
// Find the enclosing article container
|
||||
$articleId = $this->findArticleId($body, $pos);
|
||||
if (!$articleId) continue;
|
||||
|
||||
$fields = $this->getFieldValues($articleId);
|
||||
if (!$fields) continue;
|
||||
|
||||
$box = $this->buildInfobox($fields);
|
||||
if ($box) {
|
||||
$replacements[] = ['pos' => $pos, 'len' => 14, 'html' => $box];
|
||||
}
|
||||
}
|
||||
|
||||
// Apply in reverse order
|
||||
foreach (array_reverse($replacements) as $r) {
|
||||
$body = substr_replace($body, $r['html'], $r['pos'], $r['len']);
|
||||
}
|
||||
|
||||
return $body;
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-inject wertungsbox at end of article content
|
||||
*/
|
||||
private function processWertungsbox(string $body): string
|
||||
{
|
||||
// Find article containers
|
||||
if (!preg_match_all(
|
||||
'/<div\s+class="[^"]*com-content-article[^"]*"[^>]*>/',
|
||||
$body, $matches, PREG_OFFSET_CAPTURE
|
||||
)) {
|
||||
return $body;
|
||||
}
|
||||
|
||||
// Get authorized categories from plugin params
|
||||
$allowedCategories = $this->params->get('categories_wertung', []);
|
||||
if (empty($allowedCategories) || (is_array($allowedCategories) && count($allowedCategories) === 1 && empty($allowedCategories[0]))) {
|
||||
return $body; // No categories selected → don't inject
|
||||
}
|
||||
|
||||
$injections = [];
|
||||
foreach ($matches[0] as $match) {
|
||||
$containerStart = $match[1];
|
||||
$articleId = $this->findArticleId($body, $containerStart);
|
||||
if (!$articleId) continue;
|
||||
|
||||
// Check category
|
||||
$catId = $this->getArticleCategory($articleId);
|
||||
if (!$catId || !in_array((string)$catId, (array)$allowedCategories)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$fields = $this->getFieldValues($articleId);
|
||||
if (!$fields) continue;
|
||||
|
||||
$box = $this->buildWertungsbox($fields);
|
||||
if (!$box) continue;
|
||||
|
||||
// Find </article> or </div> that closes the article content
|
||||
// Inject just before the closing of the article container
|
||||
$closePos = $this->findArticleContentEnd($body, $containerStart);
|
||||
if ($closePos) {
|
||||
$injections[] = ['pos' => $closePos, 'html' => $box];
|
||||
}
|
||||
}
|
||||
|
||||
foreach (array_reverse($injections) as $inj) {
|
||||
$body = substr_replace($body, $inj['html'], $inj['pos'], 0);
|
||||
}
|
||||
|
||||
return $body;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find article ID from HTML context near a position
|
||||
*/
|
||||
private function findArticleId(string $body, int $nearPos): ?int
|
||||
{
|
||||
$chunk = substr($body, max(0, $nearPos - 500), 3000);
|
||||
|
||||
$patterns = [
|
||||
'/data-id="(\d+)"/',
|
||||
'/itemprop="identifier"[^>]*content="(\d+)"/',
|
||||
'/<meta\s+itemprop="identifier"\s+content="(\d+)"/',
|
||||
];
|
||||
|
||||
foreach ($patterns as $p) {
|
||||
if (preg_match($p, $chunk, $m)) {
|
||||
return (int)$m[1];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the closing position of article content (before the kasten/quellen div)
|
||||
*/
|
||||
private function findArticleContentEnd(string $body, int $containerStart): ?int
|
||||
{
|
||||
// Look for </article> tag, or the </div> before "Quellen" kasten
|
||||
$searchArea = substr($body, $containerStart, 20000);
|
||||
|
||||
// Try </article> first
|
||||
$articleEnd = strpos($searchArea, '</article>');
|
||||
if ($articleEnd !== false) {
|
||||
return $containerStart + $articleEnd;
|
||||
}
|
||||
|
||||
// Try the last </div> before "Quellen" heading
|
||||
if (preg_match('/<h2[^>]*>\s*Quellen\s*<\/h2>/', $searchArea, $m, PREG_OFFSET_CAPTURE)) {
|
||||
$quellenPos = $m[0][1];
|
||||
// Find the closing </div> just before Quellen
|
||||
$before = substr($searchArea, 0, $quellenPos);
|
||||
$lastDiv = strrpos($before, '</div>');
|
||||
if ($lastDiv !== false) {
|
||||
return $containerStart + $lastDiv;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get article's category ID
|
||||
*/
|
||||
private function getArticleCategory(int $articleId): ?int
|
||||
{
|
||||
try {
|
||||
$db = Factory::getContainer()->get(DatabaseInterface::class);
|
||||
$query = $db->getQuery(true)
|
||||
->select('catid')
|
||||
->from('#__content')
|
||||
->where('id = ' . (int)$articleId)
|
||||
->setLimit(1);
|
||||
$db->setQuery($query);
|
||||
return (int)$db->loadResult() ?: null;
|
||||
} catch (\Throwable $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read custom field values for an article
|
||||
*/
|
||||
private function getFieldValues(int $articleId): ?array
|
||||
{
|
||||
try {
|
||||
$db = Factory::getContainer()->get(DatabaseInterface::class);
|
||||
|
||||
$names = array_keys(self::FIELDS);
|
||||
$quotedNames = array_map(function($n) use ($db) {
|
||||
return $db->quote($n);
|
||||
}, $names);
|
||||
|
||||
$query = $db->getQuery(true)
|
||||
->select(['f.name', 'v.value'])
|
||||
->from($db->quoteName('#__fields', 'f'))
|
||||
->join('LEFT', $db->quoteName('#__fields_values', 'v'),
|
||||
'f.id = v.field_id AND v.item_id = ' . (int)$articleId)
|
||||
->where('f.context = ' . $db->quote('com_content.article'))
|
||||
->where('f.name IN (' . implode(',', $quotedNames) . ')')
|
||||
->where('f.state = 1');
|
||||
|
||||
$db->setQuery($query);
|
||||
$rows = $db->loadAssocList('name');
|
||||
|
||||
if (empty($rows)) return null;
|
||||
|
||||
$fields = [];
|
||||
$hasAny = false;
|
||||
foreach (self::FIELDS as $name => $def) {
|
||||
$val = isset($rows[$name]) && $rows[$name]['value'] !== null
|
||||
? trim($rows[$name]['value']) : '';
|
||||
$fields[$name] = $val;
|
||||
if ($val !== '') $hasAny = true;
|
||||
}
|
||||
|
||||
return $hasAny ? $fields : null;
|
||||
} catch (\Throwable $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the infobox HTML
|
||||
*/
|
||||
private function buildInfobox(array $f): string
|
||||
{
|
||||
$needle = ['bsn-verlag','bsn-designer','bsn-autor','bsn-illustrator','bsn-spieler','bsn-alter','bsn-dauer'];
|
||||
$hasAny = false;
|
||||
foreach ($needle as $n) {
|
||||
if (!empty($f[$n])) { $hasAny = true; break; }
|
||||
}
|
||||
if (!$hasAny) return '';
|
||||
|
||||
$items = [];
|
||||
$labels = [
|
||||
'bsn-verlag' => 'Verlag',
|
||||
'bsn-designer' => 'Designer',
|
||||
'bsn-autor' => 'Autor',
|
||||
'bsn-illustrator' => 'Illustrator',
|
||||
'bsn-spieler' => 'Spieler',
|
||||
'bsn-alter' => 'Alter',
|
||||
'bsn-dauer' => 'Dauer',
|
||||
'bsn-bgg-rating' => 'BGG-Rating',
|
||||
'bsn-bgg-weight' => 'Schwierigkeit',
|
||||
];
|
||||
|
||||
foreach ($labels as $key => $label) {
|
||||
if (!empty($f[$key])) {
|
||||
$items[] = '<strong>' . htmlspecialchars($label) . ':</strong> '
|
||||
. htmlspecialchars($f[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
$rows = '';
|
||||
foreach ($items as $item) {
|
||||
$rows .= '<div>' . $item . '</div>';
|
||||
}
|
||||
|
||||
return <<<HTML
|
||||
<div class="bsn-infobox" style="background:#f5f0eb;border:1px solid #d4c5b2;border-radius:8px;padding:1rem 1.3rem;margin:1.5rem 0;font-size:0.9rem;line-height:1.8;">
|
||||
{$rows}
|
||||
</div>
|
||||
HTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the wertungsbox HTML
|
||||
*/
|
||||
private function buildWertungsbox(array $f): string
|
||||
{
|
||||
if (empty($f['bsn-wertung-titel'])) return '';
|
||||
|
||||
$titel = htmlspecialchars($f['bsn-wertung-titel']);
|
||||
$subtitel = htmlspecialchars($f['bsn-wertung-untertitel']);
|
||||
$komplex = htmlspecialchars($f['bsn-wertung-komplex']);
|
||||
$sprache = htmlspecialchars($f['bsn-wertung-sprache']);
|
||||
$mechanik = $this->parseLines($f['bsn-wertung-mechanik']);
|
||||
$pro = $this->parseLines($f['bsn-wertung-pro']);
|
||||
$kontra = $this->parseLines($f['bsn-wertung-kontra']);
|
||||
$rezensent = htmlspecialchars($f['bsn-wertung-autor']);
|
||||
$punkte = (int)$f['bsn-wertung-punkte'];
|
||||
|
||||
$meta = '';
|
||||
if ($komplex) $meta .= 'Komplexität: <strong>' . $komplex . '</strong>';
|
||||
if ($sprache) {
|
||||
if ($meta) $meta .= '<br>';
|
||||
$meta .= 'Sprachen: <strong>' . $sprache . '</strong>';
|
||||
}
|
||||
|
||||
return <<<HTML
|
||||
<style>
|
||||
.bsn-wertungsbox{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;max-width:720px;margin:1.5rem 0;line-height:1.4}
|
||||
.bsn-wertungsbox *{box-sizing:border-box}
|
||||
.bsn-wb-header{background:#1a1e6b;color:#fff;padding:1.2rem 1.5rem;display:flex;justify-content:space-between;align-items:center;gap:1rem;flex-wrap:wrap}
|
||||
.bsn-wb-header .bsn-wb-titel-block{flex:1;min-width:200px}
|
||||
.bsn-wb-header .bsn-wb-titel-block .bsn-wb-titel{margin:0;font-size:1.4rem;font-weight:700}
|
||||
.bsn-wb-header .bsn-wb-titel-block .bsn-wb-untertitel{font-size:.9rem;opacity:.8;margin-top:2px}
|
||||
.bsn-wb-header .bsn-wb-meta{text-align:right;font-size:.85rem;line-height:1.6;min-width:150px}
|
||||
.bsn-wb-header .bsn-wb-logo{font-weight:700;font-size:.9rem;letter-spacing:.05em;text-align:right;line-height:1.3}
|
||||
.bsn-wb-content{background:#fff;display:flex;border:1px solid #ddd;border-top:none;flex-wrap:wrap}
|
||||
.bsn-wb-col{flex:1;min-width:200px;padding:1rem 1.2rem 1.2rem}
|
||||
.bsn-wb-col h4{margin:0 0 .6rem;font-size:.8rem;letter-spacing:.08em;color:#1a1e6b;text-align:center;text-transform:uppercase}
|
||||
.bsn-wb-col ul{list-style:none;padding:0;margin:0}
|
||||
.bsn-wb-col li{padding:.5rem 0;border-bottom:1px dotted #ccc;font-size:.85rem}
|
||||
.bsn-wb-col li:last-child{border-bottom:none}
|
||||
.bsn-wb-footer{background:#1a1e6b;color:#fff;padding:.9rem 1.5rem;display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:1rem}
|
||||
.bsn-wb-footer .bsn-wb-rezensent{font-size:.85rem}
|
||||
.bsn-wb-footer .bsn-wb-rezensent .bsn-wb-name{font-size:1.1rem;font-style:italic}
|
||||
.bsn-wb-footer .bsn-wb-score{text-align:right}
|
||||
.bsn-wb-footer .bsn-wb-score .bsn-wb-score-label{font-size:.75rem;opacity:.8;text-transform:uppercase}
|
||||
.bsn-wb-footer .bsn-wb-score .bsn-wb-score-zahl{font-size:2rem;font-weight:700}
|
||||
@media(max-width:600px){.bsn-wb-col{min-width:100%}.bsn-wb-header{flex-direction:column;text-align:center}.bsn-wb-header .bsn-wb-meta{text-align:center}.bsn-wb-header .bsn-wb-logo{text-align:center}}
|
||||
</style>
|
||||
<div class="bsn-wertungsbox">
|
||||
<div class="bsn-wb-header">
|
||||
<div class="bsn-wb-titel-block">
|
||||
<div class="bsn-wb-titel">{$titel}</div>
|
||||
<div class="bsn-wb-untertitel">{$subtitel}</div>
|
||||
</div>
|
||||
<div class="bsn-wb-meta">{$meta}</div>
|
||||
<div class="bsn-wb-logo">BRETT<br>SPIEL<br>NEWS</div>
|
||||
</div>
|
||||
<div class="bsn-wb-content">
|
||||
{$this->renderWbCol('MECHANISMEN', $mechanik)}
|
||||
{$this->renderWbCol('PRO', $pro)}
|
||||
{$this->renderWbCol('KONTRA', $kontra)}
|
||||
</div>
|
||||
<div class="bsn-wb-footer">
|
||||
<div class="bsn-wb-rezensent">
|
||||
Bewertung von:<br>
|
||||
<span class="bsn-wb-name">{$rezensent}</span>
|
||||
</div>
|
||||
<div class="bsn-wb-score">
|
||||
<div class="bsn-wb-score-label">MEINE WERTUNG</div>
|
||||
<div class="bsn-wb-score-zahl">{$punkte} / 100</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
HTML;
|
||||
}
|
||||
|
||||
private function renderWbCol(string $title, array $items): string
|
||||
{
|
||||
$h = htmlspecialchars($title);
|
||||
$lis = '';
|
||||
foreach ($items as $item) {
|
||||
$lis .= '<li>' . htmlspecialchars($item) . '</li>';
|
||||
}
|
||||
return <<<HTML
|
||||
<div class="bsn-wb-col">
|
||||
<h4>{$h}</h4>
|
||||
<ul>{$lis}</ul>
|
||||
</div>
|
||||
HTML;
|
||||
}
|
||||
|
||||
private function parseLines(string $text): array
|
||||
{
|
||||
$text = trim($text);
|
||||
if ($text === '') return [];
|
||||
$lines = preg_split('/\r?\n/', $text);
|
||||
return array_values(array_filter(array_map('trim', $lines), fn($l) => $l !== ''));
|
||||
}
|
||||
|
||||
// ─── INSTALL / UNINSTALL ───────────────────────────────
|
||||
|
||||
public function installFields(): bool
|
||||
{
|
||||
try {
|
||||
$db = Factory::getContainer()->get(DatabaseInterface::class);
|
||||
|
||||
// Find or create field group
|
||||
$query = $db->getQuery(true)
|
||||
->select('id')->from('#__fields_groups')
|
||||
->where('context = ' . $db->quote('com_content.article'))
|
||||
->where('title = ' . $db->quote('BSN Wertungsbox'))
|
||||
->setLimit(1);
|
||||
$db->setQuery($query);
|
||||
$groupId = (int)$db->loadResult();
|
||||
|
||||
if (!$groupId) {
|
||||
$db->insertObject('#__fields_groups', (object)[
|
||||
'id' => 0, 'asset_id' => 0,
|
||||
'context' => 'com_content.article',
|
||||
'title' => 'BSN Wertungsbox',
|
||||
'state' => 1, 'access' => 1,
|
||||
'language' => '*', 'params' => '{}',
|
||||
'created' => date('Y-m-d H:i:s'), 'created_by' => 0,
|
||||
], 'id');
|
||||
$groupId = (int)$db->insertid();
|
||||
}
|
||||
|
||||
$order = 1;
|
||||
foreach (self::FIELDS as $name => $def) {
|
||||
$query = $db->getQuery(true)
|
||||
->select('id')->from('#__fields')
|
||||
->where('name = ' . $db->quote($name))
|
||||
->where('context = ' . $db->quote('com_content.article'));
|
||||
$db->setQuery($query);
|
||||
if ($db->loadResult()) { $order++; continue; }
|
||||
|
||||
$params = match($def['type']) {
|
||||
'integer' => '{"class":"","default":"","first":"0","last":"100","step":"1","showlabel":"1"}',
|
||||
'textarea' => '{"class":"","rows":"4","cols":"20","filter":"raw","showlabel":"1"}',
|
||||
default => '{"class":"","showlabel":"1"}',
|
||||
};
|
||||
|
||||
$db->insertObject('#__fields', (object)[
|
||||
'id' => 0, 'asset_id' => 0,
|
||||
'context' => 'com_content.article',
|
||||
'group_id' => $groupId,
|
||||
'title' => $def['label'], 'name' => $name,
|
||||
'label' => $def['label'], 'type' => $def['type'],
|
||||
'state' => 1, 'required' => 0,
|
||||
'default_value' => '', 'params' => $params,
|
||||
'fieldparams' => '{}', 'access' => 1,
|
||||
'language' => '*', 'ordering' => $order++,
|
||||
'created' => date('Y-m-d H:i:s'), 'created_by' => 0,
|
||||
]);
|
||||
}
|
||||
return true;
|
||||
} catch (\Throwable $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function uninstallFields(): bool
|
||||
{
|
||||
try {
|
||||
$db = Factory::getContainer()->get(DatabaseInterface::class);
|
||||
$names = array_keys(self::FIELDS);
|
||||
$quoted = array_map(fn($n) => $db->quote($n), $names);
|
||||
|
||||
// Delete values first
|
||||
$query = $db->getQuery(true)
|
||||
->delete('#__fields_values')
|
||||
->where('field_id IN (SELECT id FROM ' . $db->quoteName('#__fields')
|
||||
. ' WHERE context = ' . $db->quote('com_content.article')
|
||||
. ' AND name IN (' . implode(',', $quoted) . '))');
|
||||
$db->setQuery($query);
|
||||
$db->execute();
|
||||
|
||||
// Delete fields
|
||||
$query = $db->getQuery(true)
|
||||
->delete('#__fields')
|
||||
->where('context = ' . $db->quote('com_content.article'))
|
||||
->where('name IN (' . implode(',', $quoted) . ')');
|
||||
$db->setQuery($query);
|
||||
$db->execute();
|
||||
|
||||
return true;
|
||||
} catch (\Throwable $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<extension type="plugin" group="system" method="upgrade">
|
||||
<name>plg_system_bsn_wertungsbox</name>
|
||||
<version>2.0.0</version>
|
||||
<creationDate>2026-06-18</creationDate>
|
||||
<author>brettspiel-news.de</author>
|
||||
<authorEmail>daniel@brettspiel-news.de</authorEmail>
|
||||
<copyright>(C) 2026 brettspiel-news.de</copyright>
|
||||
<license>GNU GPL v2 or later</license>
|
||||
<description>PLG_SYSTEM_BSN_WERTUNGSBOX_DESC</description>
|
||||
|
||||
<files>
|
||||
<filename plugin="bsn_wertungsbox">bsn_wertungsbox.php</filename>
|
||||
<filename>script.php</filename>
|
||||
<folder>language</folder>
|
||||
</files>
|
||||
|
||||
<languages>
|
||||
<language tag="de-DE">language/de-DE/plg_system_bsn_wertungsbox.ini</language>
|
||||
<language tag="de-DE">language/de-DE/plg_system_bsn_wertungsbox.sys.ini</language>
|
||||
</languages>
|
||||
|
||||
<config>
|
||||
<fields name="params">
|
||||
<fieldset name="basic" label="PLG_SYSTEM_BSN_WERTUNGSBOX_SETTINGS">
|
||||
<field
|
||||
name="categories_wertung"
|
||||
type="category"
|
||||
label="PLG_SYSTEM_BSN_WERTUNGSBOX_CAT_WERTUNG"
|
||||
description="PLG_SYSTEM_BSN_WERTUNGSBOX_CAT_WERTUNG_DESC"
|
||||
extension="com_content"
|
||||
multiple="true"
|
||||
size="8"
|
||||
layout="joomla.form.field.list-fancy-select"
|
||||
/>
|
||||
<field
|
||||
name="intro_count"
|
||||
type="number"
|
||||
label="PLG_SYSTEM_BSN_WERTUNGSBOX_INTRO"
|
||||
description="PLG_SYSTEM_BSN_WERTUNGSBOX_INTRO_DESC"
|
||||
default="1"
|
||||
min="0"
|
||||
max="10"
|
||||
filter="integer"
|
||||
/>
|
||||
</fieldset>
|
||||
</fields>
|
||||
</config>
|
||||
</extension>
|
||||
@@ -0,0 +1,7 @@
|
||||
PLG_SYSTEM_BSN_WERTUNGSBOX="System - BSN Wertungsbox"
|
||||
PLG_SYSTEM_BSN_WERTUNGSBOX_DESC="Rendert Infobox (per Shortcode) + Wertungsbox (automatisch) aus Custom Fields. Kategorie-gesteuert. Astroid-kompatibel."
|
||||
PLG_SYSTEM_BSN_WERTUNGSBOX_SETTINGS="Einstellungen"
|
||||
PLG_SYSTEM_BSN_WERTUNGSBOX_CAT_WERTUNG="Kategorien für Wertungsbox"
|
||||
PLG_SYSTEM_BSN_WERTUNGSBOX_CAT_WERTUNG_DESC="Nur in diesen Kategorien erscheint die Wertungsbox automatisch am Artikel-Ende. Für andere Kategorien (z.B. News) bleibt sie ausgeblendet."
|
||||
PLG_SYSTEM_BSN_WERTUNGSBOX_INTRO="Infobox-Position"
|
||||
PLG_SYSTEM_BSN_WERTUNGSBOX_INTRO_DESC="Anzahl der Absätze vor der Infobox. Standard: 1 (Infobox erscheint nach dem ersten Absatz)."
|
||||
@@ -0,0 +1,2 @@
|
||||
PLG_SYSTEM_BSN_WERTUNGSBOX="System - BSN Wertungsbox"
|
||||
PLG_SYSTEM_BSN_WERTUNGSBOX_DESC="Rendert Infobox (per Shortcode) + Wertungsbox (automatisch) aus Custom Fields. Kategorie-gesteuert. Astroid-kompatibel."
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
/**
|
||||
* Install/Uninstall Handler for BSN Wertungsbox
|
||||
*/
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Installer\InstallerAdapter;
|
||||
|
||||
class PlgSystemBsn_WertungsboxInstallerScript
|
||||
{
|
||||
public function postflight(string $type, InstallerAdapter $parent): bool
|
||||
{
|
||||
if ($type === 'install' || $type === 'update') {
|
||||
require_once __DIR__ . '/bsn_wertungsbox.php';
|
||||
$plugin = new PlgSystemBsn_Wertungsbox($parent->getParent(), []);
|
||||
$ok = $plugin->installFields();
|
||||
|
||||
if ($type === 'install') {
|
||||
if ($ok) {
|
||||
echo '<div style="background:#e8f5e9;padding:1rem;border-left:4px solid #4caf50;margin:1rem 0">';
|
||||
echo '<strong>✅ BSN Wertungsbox installiert!</strong><br>';
|
||||
echo '19 Custom Fields wurden angelegt.<br><br>';
|
||||
echo '→ <strong>Nächster Schritt:</strong> Plugin aktivieren (System → Plugins)<br>';
|
||||
echo '→ Dann Kategorien für die Wertungsbox auswählen (Plugin-Einstellungen)<br>';
|
||||
echo '</div>';
|
||||
} else {
|
||||
echo '<div style="background:#fff3e0;padding:1rem;border-left:4px solid #ff9800;margin:1rem 0">';
|
||||
echo '<strong>⚠️ Plugin installiert, aber Custom Fields konnten nicht angelegt werden.</strong><br>';
|
||||
echo 'Bitte manuell prüfen: Inhalt → Felder</div>';
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public function uninstall(InstallerAdapter $parent): bool
|
||||
{
|
||||
require_once __DIR__ . '/bsn_wertungsbox.php';
|
||||
$plugin = new PlgSystemBsn_Wertungsbox($parent->getParent(), []);
|
||||
$plugin->uninstallFields();
|
||||
|
||||
echo '<div style="background:#fff3e0;padding:1rem;border-left:4px solid:#ff9800;margin:1rem 0">';
|
||||
echo '<strong>🗑 BSN Wertungsbox deinstalliert.</strong><br>';
|
||||
echo 'Custom Fields + Werte wurden entfernt. Artikel-Inhalte sind unverändert.</div>';
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user