Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
f79377d
fix: genre filter 500 error and clickable genre hierarchy (#90)
fabiodalez-dev Mar 9, 2026
9395e3b
fix: add description to header search and fix CSV export (#83)
fabiodalez-dev Mar 9, 2026
c8e6b07
feat: inline PDF viewer and ePub fix (Issue #80)
fabiodalez-dev Mar 9, 2026
f74d955
fix: auto-register new plugin hooks on page load
fabiodalez-dev Mar 9, 2026
4daf300
fix: simplify migration SQL for manual-upgrade compatibility
fabiodalez-dev Mar 9, 2026
a3c7cfe
fix: address CodeRabbit review + update README for v0.4.9.9
fabiodalez-dev Mar 9, 2026
cbe455e
fix: add lazy backfill for descrizione_plain on first read
fabiodalez-dev Mar 9, 2026
4f75c7c
feat: social sharing buttons on book detail page (Issue #84)
fabiodalez-dev Mar 10, 2026
296cd58
fix: address CodeRabbit review on social sharing
fabiodalez-dev Mar 10, 2026
10461df
fix: add descrizione_plain to schema.sql for fresh installs
fabiodalez-dev Mar 10, 2026
038d783
fix: add descrizione_plain safety check to migrate_0.5.0
fabiodalez-dev Mar 10, 2026
4ff3781
feat: add 7 social providers + fix OG meta tags for book sharing
fabiodalez-dev Mar 10, 2026
e10893e
fix: address CodeRabbit review + add sharing E2E tests
fabiodalez-dev Mar 10, 2026
b353751
fix: address CodeRabbit review round 2
fabiodalez-dev Mar 10, 2026
a386e0d
fix: address CodeRabbit review round 3
fabiodalez-dev Mar 10, 2026
7b505d2
fix: address CodeRabbit review round 4
fabiodalez-dev Mar 10, 2026
4bb21bd
fix: include sottogenere_id in sidebar genre tree counts
fabiodalez-dev Mar 10, 2026
ea69a2e
fix: address CodeRabbit review round 5
fabiodalez-dev Mar 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 38 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

Pinakes is a self-hosted, full-featured ILS for schools, municipalities, and private collections. It focuses on automation, extensibility, and a usable public catalog without requiring a web team.

[![Version](https://img.shields.io/badge/version-0.4.9.8-0ea5e9?style=for-the-badge)](version.json)
[![Version](https://img.shields.io/badge/version-0.4.9.9-0ea5e9?style=for-the-badge)](version.json)
[![Installer Ready](https://img.shields.io/badge/one--click_install-ready-22c55e?style=for-the-badge&logo=azurepipelines&logoColor=white)](installer)
[![License](https://img.shields.io/badge/License-GPL--3.0-orange?style=for-the-badge)](LICENSE)

Expand All @@ -24,7 +24,41 @@ Pinakes is a self-hosted, full-featured ILS for schools, municipalities, and pri

---

## What's New in v0.4.9.8
## What's New in v0.4.9.9

### 📖 Inline PDF Viewer, Search Improvements & Bug Fixes

**Digital Library Plugin v1.3.0 — Inline PDF Viewer (Issue #80):**
- **Inline PDF reader** — Browser-native `<iframe>` PDF viewer on book detail pages (zero dependencies — uses Chrome's PDFium, Firefox's PDF.js, etc.)
- **Lazy-loaded iframe** — PDF is fetched only when the viewer is opened (MutationObserver pattern), no performance impact on page load
- **ePub fix** — ePub downloads now open in a new tab (`target="_blank"`) instead of navigating away from the page
- **Accessible toggle buttons** — `aria-controls` and `aria-expanded` attributes on PDF viewer and audiobook player toggles
- **Auto-hook registration** — New plugin hooks are auto-registered on page load if missing from the database

**Search Improvements (Issue #83):**
- **Description-inclusive search** — Header search, admin book search, and unified search all query `COALESCE(descrizione_plain, descrizione)` so description-only matches are returned
- **HTML-free search column** — New `descrizione_plain` column stores a `strip_tags()` version of the description. New and edited rows search against plain text, so HTML tag names stop polluting results once the column has been populated (existing rows use COALESCE fallback until backfilled)

**Database Migration (`migrate_0.4.9.9.sql`):**
- Adds `descrizione_plain TEXT DEFAULT NULL` column to `libri` table
- Fully idempotent with `INFORMATION_SCHEMA` check
- PHP backfill via BookRepository on create/update; COALESCE fallback for existing rows

**Bug Fixes:**
- **Genre filter 500 error** — Fixed subgenre filtering that caused HTTP 500 in catalog (Issue #90)
- **Clickable genre hierarchy** — Genre breadcrumbs are now clickable links for filtering
- **CSV export cleanup** — `descrizione` now follows `sottotitolo`, HTML tags stripped for clean output

**Translations:**
- All PDF viewer strings translated in Italian, English, and German
- Browser-agnostic hint text (no platform-specific keyboard shortcuts)

---

## Previous Releases

<details>
<summary><strong>v0.4.9.8</strong> - Security, Database Integrity & Code Quality</summary>

### 🔒 Security, Database Integrity & Code Quality

Expand All @@ -51,9 +85,7 @@ Pinakes is a self-hosted, full-featured ILS for schools, municipalities, and pri
- **ON DELETE SET NULL rationale** — Documented why soft-deleted book rows are safe for genre deletion (FK auto-nullifies)
- **Email notification test suite** — 16 Playwright E2E tests covering all email types via Mailpit (both SMTP and phpmail drivers)

---

## Previous Releases
</details>

<details>
<summary><strong>v0.4.9.7</strong> - Comprehensive Codebase Review — Security, Reliability & Code Quality</summary>
Expand Down Expand Up @@ -598,7 +630,7 @@ curl "http://yoursite.com/api/sru?operation=searchRetrieve&query=bath.isbn=97888
- **Retry logic** with exponential backoff
- **Error logging** and debugging tools

### 4. Digital Library (`digital-library-v1.2.0.zip`)
### 4. Digital Library (`digital-library-v1.3.0.zip`)
- **eBook support** (PDF, ePub) with download tracking
- **Audiobook streaming** (MP3, M4A, OGG) with HTML5 player
- **Per-item digital asset management** (unlimited files per book)
Expand Down
24 changes: 19 additions & 5 deletions app/Controllers/FrontendController.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

use App\Repositories\RecensioniRepository;
use App\Support\Branding;
use App\Support\ConfigStore;
use App\Support\HtmlHelper;
use App\Support\RouteTranslator;
use mysqli;
Expand Down Expand Up @@ -403,6 +404,7 @@ public function catalog(Request $request, Response $response, mysqli $db): Respo
LEFT JOIN generi g ON l.genere_id = g.id
LEFT JOIN generi gp ON g.parent_id = gp.id
LEFT JOIN generi gpp ON gp.parent_id = gpp.id
LEFT JOIN generi sg ON l.sottogenere_id = sg.id
WHERE l.deleted_at IS NULL
";

Expand Down Expand Up @@ -479,13 +481,14 @@ public function catalogAPI(Request $request, Response $response, mysqli $db): Re
$param_types = $where_conditions['types'];

// Query base senza JOIN con autori per evitare duplicati
// Include genre parents/grandparents to support filtering at any level
// Include genre parents/grandparents/subgenre to support filtering at any level
$base_query = "
FROM libri l
LEFT JOIN editori e ON l.editore_id = e.id
LEFT JOIN generi g ON l.genere_id = g.id
LEFT JOIN generi gp ON g.parent_id = gp.id
LEFT JOIN generi gpp ON gp.parent_id = gpp.id
LEFT JOIN generi sg ON l.sottogenere_id = sg.id
WHERE l.deleted_at IS NULL
";

Expand Down Expand Up @@ -650,6 +653,11 @@ public function bookDetail(Request $request, Response $response, mysqli $db): Re
$reviews = $recensioniRepo->getApprovedReviewsForBook($book_id);
$reviewStats = $recensioniRepo->getReviewStats($book_id);

// Social sharing
$sharingProviders = array_filter(explode(',', (string) ConfigStore::get('sharing.enabled_providers', '')));
$shareUrl = absoluteUrl(book_url($book));
$shareTitle = $book['titolo'] ?? '';

// Render template
$container = $this->container;
ob_start();
Expand Down Expand Up @@ -885,26 +893,30 @@ private function getFilterOptions(mysqli $db, array $filters = []): array
LEFT JOIN generi gf ON l.genere_id = gf.id
LEFT JOIN generi gfp ON gf.parent_id = gfp.id
LEFT JOIN generi gfpp ON gfp.parent_id = gfpp.id
LEFT JOIN generi sg ON l.sottogenere_id = sg.id
WHERE (
l.genere_id = g.id
OR l.sottogenere_id = g.id
OR l.genere_id IN (SELECT id FROM generi WHERE parent_id = g.id)
OR l.sottogenere_id IN (SELECT id FROM generi WHERE parent_id = g.id)
OR l.genere_id IN (SELECT gc.id FROM generi gc JOIN generi gp ON gc.parent_id = gp.id WHERE gp.parent_id = g.id)
OR l.sottogenere_id IN (SELECT gc.id FROM generi gc JOIN generi gp ON gc.parent_id = gp.id WHERE gp.parent_id = g.id)
)
{$whereClauseGen}
) AS cnt
FROM (
-- Select all genres that have books or are parents of genres with books
-- Select all genres that have books via genere_id or sottogenere_id
SELECT DISTINCT g.id FROM generi g
JOIN libri l ON g.id = l.genere_id AND l.deleted_at IS NULL
JOIN libri l ON (g.id = l.genere_id OR g.id = l.sottogenere_id) AND l.deleted_at IS NULL
UNION
SELECT DISTINCT gp.id FROM generi g
JOIN generi gp ON g.parent_id = gp.id
JOIN libri l ON g.id = l.genere_id AND l.deleted_at IS NULL
JOIN libri l ON (g.id = l.genere_id OR g.id = l.sottogenere_id) AND l.deleted_at IS NULL
UNION
SELECT DISTINCT gpp.id FROM generi g
JOIN generi gp ON g.parent_id = gp.id
JOIN generi gpp ON gp.parent_id = gpp.id
JOIN libri l ON g.id = l.genere_id AND l.deleted_at IS NULL
JOIN libri l ON (g.id = l.genere_id OR g.id = l.sottogenere_id) AND l.deleted_at IS NULL
) as genre_ids
JOIN generi g ON genre_ids.id = g.id
ORDER BY g.parent_id, g.nome
Expand Down Expand Up @@ -944,6 +956,7 @@ private function getFilterOptions(mysqli $db, array $filters = []): array
LEFT JOIN generi g ON l.genere_id = g.id
LEFT JOIN generi gp ON g.parent_id = gp.id
LEFT JOIN generi gpp ON gp.parent_id = gpp.id
LEFT JOIN generi sg ON l.sottogenere_id = sg.id
";
if (!empty($conditionsEd)) {
// Keep all conditions including genre filter
Expand Down Expand Up @@ -975,6 +988,7 @@ private function getFilterOptions(mysqli $db, array $filters = []): array
LEFT JOIN generi g ON l.genere_id = g.id
LEFT JOIN generi gp ON g.parent_id = gp.id
LEFT JOIN generi gpp ON gp.parent_id = gpp.id
LEFT JOIN generi sg ON l.sottogenere_id = sg.id
WHERE l.deleted_at IS NULL
";
if (!empty($conditionsAvail)) {
Expand Down
12 changes: 10 additions & 2 deletions app/Controllers/LibriController.php
Original file line number Diff line number Diff line change
Expand Up @@ -2766,14 +2766,14 @@ public function exportCsv(Request $request, Response $response, mysqli $db): Res
'ean',
'titolo',
'sottotitolo',
'descrizione',
'autori',
'editore',
'anno_pubblicazione',
'lingua',
'edizione',
'numero_pagine',
'genere',
'descrizione',
'formato',
'prezzo',
'copie_totali',
Expand All @@ -2795,21 +2795,29 @@ public function exportCsv(Request $request, Response $response, mysqli $db): Res
$row = $this->formatLibraryThingRow($libro, $anno);
} else {
// Standard format (default)
// Block-aware HTML→plain text for clean CSV output
$rawDesc = (string) ($libro['descrizione'] ?? '');
$rawDesc = preg_replace('/<(?:\/?(?:p|div|li|ul|ol|h[1-6]|blockquote|tr|th|td)\b[^>]*|br\b[^>]*\/?)>/i', "\n", $rawDesc);
$rawDesc = html_entity_decode(strip_tags((string) $rawDesc), ENT_QUOTES | ENT_HTML5, 'UTF-8');
$rawDesc = str_replace("\xC2\xA0", ' ', (string) $rawDesc);
$rawDesc = (string) preg_replace("/[ \t]+/", ' ', $rawDesc);
$rawDesc = (string) preg_replace("/\n{3,}/", "\n\n", $rawDesc);
$descrizione = trim($rawDesc);
$row = [
$libro['id'] ?? '',
$libro['isbn10'] ?? '',
$libro['isbn13'] ?? '',
$libro['ean'] ?? '',
$libro['titolo'] ?? '',
$libro['sottotitolo'] ?? '',
$descrizione,
$libro['autori_nomi'] ?? '',
$libro['editore_nome'] ?? '',
$anno,
$libro['lingua'] ?? '',
$libro['edizione'] ?? '',
$libro['numero_pagine'] ?? '',
$libro['genere_nome'] ?? '',
$libro['descrizione'] ?? '',
$libro['formato'] ?? '',
$libro['prezzo'] ?? '',
$libro['copie_totali'] ?? '1',
Expand Down
14 changes: 7 additions & 7 deletions app/Controllers/SearchController.php
Original file line number Diff line number Diff line change
Expand Up @@ -128,10 +128,10 @@ public function books(Request $request, Response $response, mysqli $db): Respons
l.copie_disponibili,
l.copie_totali
FROM libri l
WHERE l.deleted_at IS NULL AND (l.titolo LIKE ? OR l.sottotitolo LIKE ? OR l.isbn10 LIKE ? OR l.isbn13 LIKE ? OR l.ean LIKE ?)
WHERE l.deleted_at IS NULL AND (l.titolo LIKE ? OR l.sottotitolo LIKE ? OR l.isbn10 LIKE ? OR l.isbn13 LIKE ? OR l.ean LIKE ? OR COALESCE(l.descrizione_plain, l.descrizione) LIKE ?)
ORDER BY l.titolo
");
$stmt->bind_param('sssss', $s, $s, $s, $s, $s);
$stmt->bind_param('ssssss', $s, $s, $s, $s, $s, $s);
Comment on lines +131 to +134
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Guard descrizione_plain during rollout.

These queries now hard-reference l.descrizione_plain, but the rest of the PR still treats that column as optional. If an app node serves this code before the migration has run, these endpoints will fail with Unknown column 'l.descrizione_plain'. Build the predicate behind a schema check, or fall back to l.descrizione until the migration is guaranteed complete everywhere.

Also applies to: 238-241, 351-354

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Controllers/SearchController.php` around lines 131 - 134, The query
references l.descrizione_plain directly which will fail if the migration hasn't
run; modify SearchController to detect the column at runtime (e.g., run a single
lightweight check like SHOW COLUMNS FROM libri LIKE 'descrizione_plain' or query
INFORMATION_SCHEMA once on startup) and then build the SQL accordingly: if the
column exists use COALESCE(l.descrizione_plain, l.descrizione) in the WHERE
clause, otherwise use l.descrizione; update the SQL-building sites (the shown
WHERE/ORDER block and the other occurrences at the ranges you noted around lines
238-241 and 351-354) and ensure the prepared statement creation/bind_param calls
(the $stmt and bind_param usage) use the same parameter count and placeholders
as the dynamically-built SQL.

$stmt->execute();
$res = $stmt->get_result();
while ($r = $res->fetch_assoc()) {
Expand Down Expand Up @@ -227,18 +227,18 @@ private function searchBooks(mysqli $db, string $query): array
$results = [];
$s = '%'.$query.'%';

// Search by ISBN, EAN, title, subtitle - include author via subquery
// Search by ISBN, EAN, title, subtitle, description - include author via subquery
$stmt = $db->prepare("
SELECT l.id, l.titolo AS label, l.isbn10, l.isbn13, l.ean,
(SELECT GROUP_CONCAT(a.nome ORDER BY la.ruolo='principale' DESC, a.nome SEPARATOR ', ')
FROM libri_autori la
JOIN autori a ON la.autore_id = a.id
WHERE la.libro_id = l.id) AS autori
FROM libri l
WHERE l.deleted_at IS NULL AND (l.isbn10 LIKE ? OR l.isbn13 LIKE ? OR l.ean LIKE ? OR l.titolo LIKE ? OR l.sottotitolo LIKE ?)
WHERE l.deleted_at IS NULL AND (l.isbn10 LIKE ? OR l.isbn13 LIKE ? OR l.ean LIKE ? OR l.titolo LIKE ? OR l.sottotitolo LIKE ? OR COALESCE(l.descrizione_plain, l.descrizione) LIKE ?)
ORDER BY l.titolo LIMIT 10
");
$stmt->bind_param('sssss', $s, $s, $s, $s, $s);
$stmt->bind_param('ssssss', $s, $s, $s, $s, $s, $s);
$stmt->execute();
$res = $stmt->get_result();

Expand Down Expand Up @@ -348,10 +348,10 @@ private function searchBooksWithDetails(mysqli $db, string $query): array
FROM libri l
LEFT JOIN libri_autori la ON l.id = la.libro_id
LEFT JOIN autori a ON la.autore_id = a.id
WHERE l.deleted_at IS NULL AND (l.titolo LIKE ? OR l.sottotitolo LIKE ? OR l.isbn10 LIKE ? OR l.isbn13 LIKE ? OR l.ean LIKE ? OR a.nome LIKE ?)
WHERE l.deleted_at IS NULL AND (l.titolo LIKE ? OR l.sottotitolo LIKE ? OR COALESCE(l.descrizione_plain, l.descrizione) LIKE ? OR l.isbn10 LIKE ? OR l.isbn13 LIKE ? OR l.ean LIKE ? OR a.nome LIKE ?)
ORDER BY l.titolo LIMIT 8
");
$stmt->bind_param('ssssss', $s, $s, $s, $s, $s, $s);
$stmt->bind_param('sssssss', $s, $s, $s, $s, $s, $s, $s);
$stmt->execute();
$res = $stmt->get_result();

Expand Down
24 changes: 24 additions & 0 deletions app/Controllers/SettingsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -892,6 +892,30 @@ private function getCookieBannerTextFieldMap(): array
];
}

public function updateSharingSettings(Request $request, Response $response, mysqli $db): Response
{
$data = (array) $request->getParsedBody();
// CSRF validated by CsrfMiddleware

$repository = new SettingsRepository($db);
$repository->ensureTables();

$allowedSlugs = ['facebook', 'x', 'whatsapp', 'telegram', 'linkedin', 'reddit', 'pinterest', 'threads', 'bluesky', 'tumblr', 'pocket', 'vk', 'line', 'sms', 'email', 'copylink'];
$selected = $data['sharing_providers'] ?? [];
if (!is_array($selected)) {
$selected = [];
}
$selected = array_map(static fn($s) => strtolower(trim((string) $s)), $selected);
$valid = array_values(array_intersect($allowedSlugs, array_unique($selected)));
$value = implode(',', $valid);

$repository->set('sharing', 'enabled_providers', $value);
ConfigStore::set('sharing.enabled_providers', $value);

$_SESSION['success_message'] = __('Impostazioni di condivisione aggiornate.');
return $this->redirect($response, '/admin/settings?tab=sharing');
}

private function redirect(Response $response, string $location): Response
{
return $response->withHeader('Location', $location)->withStatus(302);
Expand Down
39 changes: 39 additions & 0 deletions app/Models/BookRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,20 @@ public function getById(int $id): ?array
if (!$row)
return null;

// Lazy backfill: populate descrizione_plain for pre-migration rows on first read
if ($this->hasColumn('descrizione_plain')
&& $row['descrizione_plain'] === null
&& !empty($row['descrizione'])
) {
$plain = $this->toPlainTextDescription((string)$row['descrizione']);
$upd = $this->db->prepare("UPDATE libri SET descrizione_plain = ? WHERE id = ?");
if ($upd) {
$upd->bind_param('si', $plain, $id);
$upd->execute();
}
$row['descrizione_plain'] = $plain;
}
Comment on lines +96 to +108
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Keep getById() side-effect free.

This read path now issues an UPDATE. getById() is also used from unrelated read-before-write flows such as app/Controllers/LibriController.php:1070 and app/Support/MergeHelper.php:93, so this can unexpectedly require write access and introduce locking/transaction failures in places that only asked to read the book.

♻️ Minimal fix
-        // Lazy backfill: populate descrizione_plain for pre-migration rows on first read
-        if ($this->hasColumn('descrizione_plain')
-            && $row['descrizione_plain'] === null
-            && !empty($row['descrizione'])
-        ) {
-            $plain = $this->toPlainTextDescription((string)$row['descrizione']);
-            $upd = $this->db->prepare("UPDATE libri SET descrizione_plain = ? WHERE id = ?");
-            if ($upd) {
-                $upd->bind_param('si', $plain, $id);
-                $upd->execute();
-            }
-            $row['descrizione_plain'] = $plain;
-        }
+        if ($this->hasColumn('descrizione_plain')
+            && $row['descrizione_plain'] === null
+            && !empty($row['descrizione'])
+        ) {
+            // Keep reads side-effect free; persist backfills in migrations or write paths.
+            $row['descrizione_plain'] = $this->toPlainTextDescription((string) $row['descrizione']);
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Models/BookRepository.php` around lines 96 - 108, getById() currently
performs a DB UPDATE to lazily backfill descrizione_plain (calls
toPlainTextDescription, uses $this->db->prepare and $upd->execute()), which
creates a write side-effect; remove the UPDATE and avoid executing
$upd->execute() inside getById(). Instead, compute $plain =
$this->toPlainTextDescription((string)$row['descrizione']) and assign
$row['descrizione_plain'] = $plain for the returned result only (no
bind_param/execute), or move the persistence into a separate explicit method
(e.g. backfillDescrizionePlain($id)) or a background migration job that can be
invoked where writes are allowed.


// Resolve genre hierarchy for the 3-level cascade (Radice → Genere → Sottogenere)
// Walk up the tree from genere_id to find the full ancestor chain, then map to cascade levels
$this->resolveGenreHierarchy($row);
Expand Down Expand Up @@ -282,6 +296,10 @@ public function createBasic(array $data): int
if ($this->hasColumn('descrizione')) {
$addField('descrizione', 's', $data['descrizione'] ?? null);
}
if ($this->hasColumn('descrizione_plain')) {
$raw = $data['descrizione'] ?? null;
$addField('descrizione_plain', 's', $this->toPlainTextDescription($raw));
}
if ($this->hasColumn('parole_chiave')) {
$addField('parole_chiave', 's', $data['parole_chiave'] ?? null);
}
Expand Down Expand Up @@ -608,6 +626,10 @@ public function updateBasic(int $id, array $data): bool
if ($this->hasColumn('descrizione')) {
$addSet('descrizione', 's', $data['descrizione'] ?? null);
}
if ($this->hasColumn('descrizione_plain')) {
$raw = $data['descrizione'] ?? null;
$addSet('descrizione_plain', 's', $this->toPlainTextDescription($raw));
}
if ($this->hasColumn('parole_chiave')) {
$addSet('parole_chiave', 's', $data['parole_chiave'] ?? null);
}
Expand Down Expand Up @@ -1150,6 +1172,23 @@ private function resolveGenreHierarchy(array &$row): void
}
}

private function toPlainTextDescription(?string $html): ?string
{
if ($html === null || $html === '') {
return $html;
}
$text = preg_replace(
'/<(?:\/?(?:p|div|li|ul|ol|h[1-6]|blockquote|tr|th|td)\b[^>]*|br\b[^>]*\/?)>/i',
"\n",
$html
);
$text = html_entity_decode(strip_tags((string) $text), ENT_QUOTES | ENT_HTML5, 'UTF-8');
$text = str_replace("\xC2\xA0", ' ', (string) $text);
$text = preg_replace("/[ \t]+/", ' ', (string) $text);
$text = preg_replace("/\n{3,}/", "\n\n", (string) $text);
return trim((string) $text);
}

private static array $columnCacheByDb = [];
private function hasColumn(string $name): bool
{
Expand Down
6 changes: 6 additions & 0 deletions app/Routes/web.php
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,12 @@
return $controller->updateLabels($request, $response, $db);
})->add(new CsrfMiddleware())->add(new AdminAuthMiddleware());

$app->post('/admin/settings/sharing', function ($request, $response) use ($app) {
$db = $app->getContainer()->get('db');
$controller = new SettingsController();
return $controller->updateSharingSettings($request, $response, $db);
})->add(new CsrfMiddleware())->add(new AdminAuthMiddleware());

$app->post('/admin/settings/advanced', function ($request, $response) use ($app) {
$db = $app->getContainer()->get('db');
$controller = new SettingsController();
Expand Down
10 changes: 10 additions & 0 deletions app/Support/ConfigStore.php
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,9 @@ public static function all(): array
'cms' => [
'events_page_enabled' => '1', // Default to enabled
],
'sharing' => [
'enabled_providers' => 'facebook,x,whatsapp,email',
],
];

$localizedDefaults = self::getLocaleDefaultTexts();
Expand Down Expand Up @@ -567,6 +570,13 @@ private static function loadDatabaseSettings(): array
}
}

if (!empty($raw['sharing'])) {
self::$dbSettingsCache['sharing'] = [];
foreach ($raw['sharing'] as $key => $value) {
self::$dbSettingsCache['sharing'][$key] = (string) $value;
}
}

if (!empty($raw['system'])) {
self::$dbSettingsCache['system'] = [];
foreach ($raw['system'] as $key => $value) {
Expand Down
Loading