# Theme System

# Anzeige der Revisionen in einem PDF Export

<p class="callout success">getestet mit Version **24.02**</p>

## Anforderung

Um die Revisionen und ggfs. den Changelog in einem PDF Export ganz zum Schluss anzeigen lassen zu können sind einige Anpassungen nötig.

Zuerst habe ich dafür einen zusätzlichen Link in das Export-Menü eingebaut um einmal eine Version ohne Revisionen und eine mit Revisionen exportieren zu können.

## betroffene Dateien

<p class="callout info">Dateien müssen sich in der entsprechenden Struktur unterhalb des Themes befinden.  
Ordner = *kursiv*  
Dateien = **fett**</p>

- *entities*
    - **export-menu.blade.php**
- *exports*
    - *parts*
        - **revisions-index-row-compact.blade.php**
    - **page.blade.php**

## Inhalte der Dateien

<details id="bkmrk-export-menu.blade.ph"><summary>export-menu.blade.php</summary>

Hier wurde ein weiterer Link in **Zeile 6** eingefügt, der mittels GET den Wert `history=true` übergibt. Diese GET Variable kann dann später abgefragt werden.

```html
[...]
    <ul refs="dropdown@menu" class="wide dropdown-menu" role="menu">
        <li><a href="{{ $entity->getUrl('/export/html') }}" target="_blank" class="label-item"><span>{{ trans('entities.export_html') }}</span><span>.html</span></a></li>
        <li><a href="{{ $entity->getUrl('/export/pdf') }}" target="_blank" class="label-item"><span>{{ trans('entities.export_pdf') }}</span><span>.pdf</span></a></li>
        <li><a href="{{ $entity->getUrl('/export/pdf?qr=true') }}" target="_blank" class="label-item"><span>{{ trans('entities.export_pdf') }} + QR</span><span>.pdf</span></a></li>
        <li><a href="{{ $entity->getUrl('/export/pdf?history=true') }}" target="_blank" class="label-item"><span>{{ trans('entities.export_pdf') }} + History</span><span>.pdf</span></a></li>
        <li><a href="{{ $entity->getUrl('/export/plaintext') }}" target="_blank" class="label-item"><span>{{ trans('entities.export_text') }}</span><span>.txt</span></a></li>
        <li><a href="{{ $entity->getUrl('/export/markdown') }}" target="_blank" class="label-item"><span>{{ trans('entities.export_md') }}</span><span>.md</span></a></li>
    </ul>
[...]
```

</details><details id="bkmrk-revisions-index-row-"><summary>revisions-index-row-compact.blade.php</summary>

Ich habe in dem Ordner eine weitere Datei angelegt und mit folgendem Inhalt gefüllt:

```html
<tr>
    <td>{{ $revision->created_at->isoFormat('D MMMM Y') }}</td>
    <td>{{ $revision->revision_number == 0 ? '' : $revision->revision_number }}</td>
    <td>@if($revision->createdBy) {{ $revision->createdBy->name }} @else {{ trans('common.deleted_user') }} @endif</td>
    <td>{{ $revision->summary }}</td>
</tr>
```

</details><details id="bkmrk-page.blade.php-in-di"><summary>page.blade.php</summary>

In dieser Datei habe ich an das Ende der Seite folgenden Code eingefügt und die alte Meta Ansicht deaktiviert. Der geänderte Code beginnt in **Zeile 7** (in diesem Codeschnipsel).

```html
[...]
        <div style="clear:left;"></div>

        {!! $page->renderedHTML ?? $page->html !!}
    </div>

    @if(request()->query('history'))
        <hr>    
        <h2>Dokumentenhistorie</h2>
        <table>
            <tr>
                <th>Datum</th>
                <th>Version</th>
                <th>Autor</th>
                <th>Anmerkungen</th>
            </tr>
            @if(count($page->revisions) > 0)
                @foreach($page->revisions as $index => $revision)
                    @include('exports.parts.revisions-index-row-compact', ['revision' => $revision, 'current' => $page->revision_count === $revision->revision_number])
                @endforeach
            @else
                <p>{{ trans('entities.pages_revisions_none') }}</p>
            @endif
        </table>
    @endif

    <!-- <hr>

    <div class="text-muted text-small">
        @include('exports.parts.meta', ['entity' => $page])
    </div> -->
@endsection

```

Der Codeabschnitt kann auch an jeder anderen Stelle hinterlegt werden, für mich hat es aber am Ende des Dokuments den meisten Sinn gemacht.

</details>Ich habe bei den Anpassungen auf Übersetzungen verzichtet, das würde sich aber problemlos ändern lassen.

## Screenshots

[![image.png](https://bookstack.jelinek-rz.de/uploads/images/gallery/2024-04/scaled-1680-/nsfimage.png)](https://bookstack.jelinek-rz.de/uploads/images/gallery/2024-04/nsfimage.png)

# URL mit Mausklick kopieren

<p class="callout success">getestet mit Version **24.02**</p>

## Anforderung

Wenn man Bookstack als PWA nutzt ist es nur umständlich möglich, die URL zu kopieren wenn man diese jemandem schicken möchte.

## betroffene Dateien

<p class="callout info">Dateien müssen sich in der entsprechenden Struktur unterhalb des Themes befinden.  
Ordner = *kursiv*  
Dateien = **fett**</p>

- *entities*
    - **share-link.blade.php**
- *shelves*
    - **show.blade.php**
- *books*
    - **show.blade.php**
- *chapters*
    - **show.blade.php**
- *page*
    - **show.blade.php**

## Inhalte der Dateien

<details id="bkmrk-export-menu.blade.ph"><summary>share-link.blade.php</summary>

```html
<button type="button"
        id="share-link-button"
        data-success-text="Link copied to clipboard!"
        class="icon-list-item text-link">
    <span>@icon('share')</span>
    <span>{{ trans('common.share') }}</span>
</button>
<script nonce="{{ $cspNonce }}">
    (async function() {
        const shareButton = document.getElementById('share-link-button');
        shareButton.addEventListener('click', event => {
           copyTextToClipboard(window.location.href);
           window.$events.success(shareButton.dataset.successText);
        });

        async function copyTextToClipboard(text) {
            if (window.isSecureContext && navigator.clipboard) {
                await navigator.clipboard.writeText(text);
                return;
            }

            // Backup option where we can't use the navigator.clipboard API
            const tempInput = document.createElement('textarea');
            tempInput.style = 'position: absolute; left: -1000px; top: -1000px;';
            tempInput.value = text;
            document.body.appendChild(tempInput);
            tempInput.select();
            document.execCommand('copy');
            document.body.removeChild(tempInput);
        }
    })()
</script>
```

</details><details id="bkmrk-revisions-index-row-"><summary>show.blade.php (gleich für jede Datei)</summary>

Hier wurde die Zeile 10 hinzugefügt (Zeilennummer nur für diesen Ausschnitt)

```html
            @if($watchOptions->canWatch() && !$watchOptions->isWatching())
                @include('entities.watch-action', ['entity' => $page])
            @endif
            @if(user()->hasAppAccess())
                @include('entities.favourite-action', ['entity' => $page])
            @endif
            @if(userCan('content-export'))
                @include('entities.export-menu', ['entity' => $page])
            @endif
            @include('entities.share-link', ['entity' => $page])
        </div>

    </div>
@stop
```

</details>## Screenshots

[![image.png](https://bookstack.jelinek-rz.de/uploads/images/gallery/2024-04/scaled-1680-/fUFimage.png)](https://bookstack.jelinek-rz.de/uploads/images/gallery/2024-04/fUFimage.png)

# tabellarische Darstellung der Tags in einem PDF Export

<p class="callout success">getestet mit Version **24.02**</p>

## Anforderung

Für diverse Zertifizierungsdokument wird ein "Dokumentenheader" benötigt. Da ich die Informationen in Tags versteckt habe lasse ich diese einfach als Tabelle bei einem PDF Export darstellen.

## betroffene Dateien

<p class="callout info">Dateien müssen sich in der entsprechenden Struktur unterhalb des Themes befinden.  
Ordner = *kursiv*  
Dateien = **fett**</p>

- *exports*
    - *parts*
        - **tag-export-table.blade.php**
    - **page.blade.php**

## Inhalte der Dateien

<details id="bkmrk-export-menu.blade.ph"><summary>tag-export-table.blade.php</summary>

```html
<tr>
    <th>{{ $tag->name }}</th>
    @if($tag->value)<td>{{$tag->value}}</td>@else<td>&nbsp;</td>@endif
</tr>
```

</details><details id="bkmrk-revisions-index-row-"><summary>page.blade.php</summary>

An der gewünschten Stelle muss folgender Code eingefügt werden:

```php
@if($page->tags->count() > 0)
    <h2>Dokumenteninformationen</h2>
    <table>
        @foreach($page->tags as $tag)
            @include('exports.parts.tag-export-table', ['tag' => $tag])
        @endforeach
    </table>
    </div>
    <hr>
@endif
```

</details>## Screenshots

[![image.png](https://bookstack.jelinek-rz.de/uploads/images/gallery/2024-04/scaled-1680-/d7Limage.png)](https://bookstack.jelinek-rz.de/uploads/images/gallery/2024-04/d7Limage.png)

# PDF Export mit QR-Code

<p class="callout success">getestet mit Version **24.02**</p>

## Anforderung

Ich wollte gerne die Möglichkeit haben, exportierte Daten einfach wieder in der digitalen Welt zu finden. Was ist dazu besser geeignet als ein **QR-Code**.

## betroffene Dateien

<p class="callout info">Dateien müssen sich in der entsprechenden Struktur unterhalb des Themes befinden.  
Ordner = *kursiv*  
Dateien = **fett**</p>

- *layouts*
    - *parts*
        - **export-body-start.blade.php**
- *entities*
    - **export-menu.blade.php**

## Inhalte der Dateien

<details id="bkmrk-export-body-start.bl"><summary>export-body-start.blade.php</summary>

Vor dem ersten `div`-Container muss hier folgendes eingetragen werden:

```
@inject('totp', 'BookStack\Access\Mfa\TotpService')

@php
$qrCode = $totp->generateQrCodeSvg($page->getUrl());
$imgStr = 'data:image/svg+xml;base64,' . base64_encode($qrCode);
@endphp
```

Im Anschluss kann an einer beliebigen Stelle das Bild an die PDF übergeben werden.  
Dazu muss folgender Abschnitt hinzugefügt werden:

```php
@if(request()->query('qr'))
    <div style="float: right;">
        <img width="50" src="{{ $imgStr }}" alt="{{ $page->getUrl() }}">
    </div>
@endif
```

</details><details id="bkmrk-revisions-index-row-"><summary>export-menu.blade.php</summary>

Hier muss nun einfach nach der Zeile gesucht werden mit dem Inhalt `/export/pdf`.

Danach die Zeile kopieren und den String ?qr=true anhängen an die selbe Stelle.

Es sollte dann wie folgt aussehen:

```html
<ul refs="dropdown@menu" class="wide dropdown-menu" role="menu">
      <li><a href="{{ $entity->getUrl('/export/html') }}" target="_blank" class="label-item"><span>{{ trans('entities.export_html') }}</span><span>.html</span></a></li>
      <li><a href="{{ $entity->getUrl('/export/pdf') }}" target="_blank" class="label-item"><span>{{ trans('entities.export_pdf') }}</span><span>.pdf</span></a></li>
      <li><a href="{{ $entity->getUrl('/export/pdf?qr=true') }}" target="_blank" class="label-item"><span>{{ trans('entities.export_pdf') }} + QR</span><span>.pdf</span></a></li>
      <li><a href="{{ $entity->getUrl('/export/plaintext') }}" target="_blank" class="label-item"><span>{{ trans('entities.export_text') }}</span><span>.txt</span></a></li>
      <li><a href="{{ $entity->getUrl('/export/markdown') }}" target="_blank" class="label-item"><span>{{ trans('entities.export_md') }}</span><span>.md</span></a></li>
  </ul>
```

In diesem Ausschnitt ist in Zeile 4 der neue Export Link hinzugefügt.

</details>## Screenshots

[![image.png](https://bookstack.jelinek-rz.de/uploads/images/gallery/2024-06/scaled-1680-/image.png)](https://bookstack.jelinek-rz.de/uploads/images/gallery/2024-06/image.png)

# PDF Export anpassen mit Header und Footer

<p class="callout success">getestet mit Version **25.07**</p>

## Anforderung

Die Standard-PDF Seite ist nicht wirklich ansprechend, also werden hier Header und Footer sowie Seitenzahlen eingefügt.

## betroffene Dateien

<p class="callout info">Dateien müssen sich in der entsprechenden Struktur unterhalb des Themes befinden.  
Ordner = *kursiv*  
Dateien = **fett**</p>

- *layouts*
    - *parts*
        - **export-body-start.blade.php**

## Inhalte der Dateien

<details id="bkmrk-export-body-start.bl"><summary>export-body-start.blade.php</summary>

Die Datei muss am Ende wie folgt aussehen:

```html
@if ($format === 'pdf')
<style media="print">
    .print-header-footer {
        position: fixed;
        width: 100%;
    }
    .print-footer {
        position: fixed;
        bottom: -40px;
        width: 100%;
    }
    .print-header-footer-inner {
        max-width: 840px;
        margin: 0 auto;
        color: #666;
    }
    .print-page-number:after {
        content: "Seite "counter(page);
    }
    @page {
        margin-top: 100px;
        margin-bottom: 80px;
    }
</style>

<div class="print-header-footer" style="top: -60px;">
    <div class="print-header-footer-inner">
        <div style="float: left; opacity: 0.8;">
            <img height="50" src="data:image/png;base64,{{ base64_encode(file_get_contents(theme_path('images/logo.png'))) }}">
        </div>
    </div>
    <div style="clear:both;"></div>
    <hr style="color: #ccc;">
</div>

<div class="print-header-footer" style="bottom: -40px;">
    <div class="print-header-footer-inner">
        <div style="float: left; opacity: 0.8; font-size: 8pt; text-align: left">
            &copy; [Footer-Text links]
        </div>
        <div style="float: right; opacity: 0.8; font-size: 8pt; text-align: right">
            <div class="print-page-number" style="opacity: 1"></div>
        </div>
    </div>
    <div style="clear:both;"></div>
</div>
@endif
```

</details>

# News-Seite / schwarzes Brett

<p class="callout success">getestet mit Version 24.12</p>

## Anforderung

Zum Abbilden der Funktion eines schwarzen Bretts bzw. einer News-Seite um aktuelle Infos anzuzeigen.

## betroffene Dateien

<p class="callout info">Dateien müssen sich in der entsprechenden Struktur unterhalb des Themes befinden.  
Ordner = *kursiv*  
Dateien = **fett**</p>

- *home*
    - **specific-page.blade.php**

## Inhalte der Dateien

<details id="bkmrk-export-menu.blade.ph"><summary>specific-page.blade.php</summary>

Innerhalb der `section('left')` oder `section('right')`, je nach Präferenz, muss folgender `<div>`-Block eingefügt werden:

```html
<div class="card mb-xl">
    @php
    $newsBookId = 34;
    $newsItems = \BookStack\Entities\Models\Page::visible()
        ->where('book_id', $newsBookId)
        ->orderBy('created_at', 'desc')
        ->take(7)
        ->get();
    @endphp
    <h3 class="card-title" style="font-weight: bold; font-size: 15pt;">{{ trans('common.actualnews') }}</h3>
    <div class="px-m">
        @include('entities.list', [
            'entities' => $newsItems,
            'style' => 'compact',
        ])
    </div>
    <a href="{{ url('/books/firmen-aushange-schwarzes-brett')  }}" class="card-footer-link">{{ trans('common.view_all') }}</a>
</div>
```

die ID für das Buch welches hier genutzt werden soll lässt sich einfach herausfinden, in dem ein Buch öffnet und dann die Entwicklertools startet (`F12`). Hier sucht man dann nach folgendem Begriff: `option:entity-search:entity-id`  
Direkt dahinter steht die ID des Buches, welches dann in der Anpassung hinterlegt werden muss.

</details>## Screenshots

[![image.png](https://bookstack.jelinek-rz.de/uploads/images/gallery/2025-01/scaled-1680-/image.png)](https://bookstack.jelinek-rz.de/uploads/images/gallery/2025-01/image.png)

# Regale zu denen ein Buch gehört anzeigen

<p class="callout success">getestet mit Version **25.02**</p>

## Anforderung

Ich wollte sehen in welchen Regalen ein Buch steht, da ein Buch in mehreren Regalen stehen kann und weil die Brotkrumen Navigation das Regal nicht anzeigt wenn man nicht das Buch z.B. über die Suche öffnet.

## betroffene Dateien

<p class="callout info">Dateien müssen sich in der entsprechenden Struktur unterhalb des Themes befinden.  
Ordner = *kursiv*  
Dateien = **fett**</p>

- *books*
    - **show.blade.oph**

## Inhalte der Dateien

<details id="bkmrk-export-menu.blade.ph"><summary>show.blade.php</summary>

Unter `@section('left')` einfach an der gewünschten Stelle den folgenden Code einfügen

```html
[...]
    @if(count($bookParentShelves) > 0)
        <div class="actions mb-xl">
            <h5>{{ trans('entities.shelves') }}</h5>
            @include('entities.list', ['entities' => $bookParentShelves, 'style' => 'compact'])
        </div>
    @endif
[...]
```

</details>## Screenshots

[![image.png](https://bookstack.jelinek-rz.de/uploads/images/gallery/2025-03/scaled-1680-/image.png)](https://bookstack.jelinek-rz.de/uploads/images/gallery/2025-03/image.png)

# BookStack Page Lock & PIN Protection

<p class="callout success">getestet mit Version **25.12.2**</p>

## Quelle

[https://github.com/Rockyroad1827/Bookstacks-Password-Protect-Page-Hack](https://github.com/Rockyroad1827/Bookstacks-Password-Protect-Page-Hack)

<details id="bkmrk-originale-anleitung-"><summary>originale Anleitung</summary>

# BookStack Page Lock &amp; PIN Protection

<div class="markdown-heading" dir="auto">[<svg aria-hidden="true" class="octicon octicon-link" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg>](https://github.com/Rockyroad1827/Bookstacks-Password-Protect-Page-Hack/tree/main#bookstack-page-lock--pin-protection)</div>[![BookStack](https://camo.githubusercontent.com/d3a8965b17311941c3f7cfe16ab847b9b12304d5de0fee658881c9c313afcf1a/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f426f6f6b537461636b2d4164646f6e2d3032383844313f7374796c653d666c61742d737175617265266c6f676f3d626f6f6b737461636b266c6f676f436f6c6f723d7768697465)](https://camo.githubusercontent.com/d3a8965b17311941c3f7cfe16ab847b9b12304d5de0fee658881c9c313afcf1a/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f426f6f6b537461636b2d4164646f6e2d3032383844313f7374796c653d666c61742d737175617265266c6f676f3d626f6f6b737461636b266c6f676f436f6c6f723d7768697465) [![BookStack Version](https://camo.githubusercontent.com/3b55e116c1f17c7a6a5adfcae56701e8a548a32375c7d71fd303a3c01fa5a808/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f426f6f6b537461636b2d7632352e31322e322d6c69676874626c75653f7374796c653d666c61742d737175617265266c6f676f3d626f6f6b737461636b266c6f676f436f6c6f723d7768697465)](https://camo.githubusercontent.com/3b55e116c1f17c7a6a5adfcae56701e8a548a32375c7d71fd303a3c01fa5a808/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f426f6f6b537461636b2d7632352e31322e322d6c69676874626c75653f7374796c653d666c61742d737175617265266c6f676f3d626f6f6b737461636b266c6f676f436f6c6f723d7768697465) [![PHP](https://camo.githubusercontent.com/423698a15c259fe99627b4ab8328a052b729761f9c4b01dafc58b699b85f352b/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5048502d382e302532422d3737374242343f7374796c653d666c61742d737175617265266c6f676f3d706870266c6f676f436f6c6f723d7768697465)](https://camo.githubusercontent.com/423698a15c259fe99627b4ab8328a052b729761f9c4b01dafc58b699b85f352b/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5048502d382e302532422d3737374242343f7374796c653d666c61742d737175617265266c6f676f3d706870266c6f676f436f6c6f723d7768697465) [![Security](https://camo.githubusercontent.com/15401b22d514eddee1fa7ed2ef094a829e01d49af6ce51b7edfb3401f81799a4/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f53656375726974792d50494e5f4c6f636b2d637269746963616c3f7374796c653d666c61742d737175617265)](https://camo.githubusercontent.com/15401b22d514eddee1fa7ed2ef094a829e01d49af6ce51b7edfb3401f81799a4/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f53656375726974792d50494e5f4c6f636b2d637269746963616c3f7374796c653d666c61742d737175617265) [![License](https://camo.githubusercontent.com/422db9fd40f5831c765cf6530b6750c081b696bd18d904cf89554df98c676277/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d677265656e3f7374796c653d666c61742d737175617265)](https://camo.githubusercontent.com/422db9fd40f5831c765cf6530b6750c081b696bd18d904cf89554df98c676277/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d677265656e3f7374796c653d666c61742d737175617265)

A lightweight, PIN-based protection layer for [BookStack](https://www.bookstackapp.com/). This modification allows administrators to "lock" specific pages behind a custom PIN or a global Master PIN, effectively hiding the content until the correct code is entered.

---

## Features

<div class="markdown-heading" dir="auto">[<svg aria-hidden="true" class="octicon octicon-link" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg>](https://github.com/Rockyroad1827/Bookstacks-Password-Protect-Page-Hack/tree/main#features)</div>- **Per-Page Locking:** Secure specific pages without altering global role permissions.
- **Dual PIN Modes:** Pages can use a specific "Custom Password" or fall back to a global "Master PIN" defined in your `.env` file.
- **Native UI Integration:** Controls are embedded directly into the Page "Permissions" view.
- **Visual Indicators:** Automatically adds a lock symbol (🔒) to page titles and status indicators in the UI.
- **Search &amp; Preview Scrubbing:** Prevents protected content from leaking into search results or list previews by clearing preview text for protected items.
- **CLI Management Tool:** Includes a terminal script to manage locks, update passwords, and remove protections.

---

## Installation

<div class="markdown-heading" dir="auto">[<svg aria-hidden="true" class="octicon octicon-link" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg>](https://github.com/Rockyroad1827/Bookstacks-Password-Protect-Page-Hack/tree/main#installation)</div>### 1. File Placement

<div class="markdown-heading" dir="auto">[<svg aria-hidden="true" class="octicon octicon-link" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg>](https://github.com/Rockyroad1827/Bookstacks-Password-Protect-Page-Hack/tree/main#1-file-placement)</div>This guide assumes you are using the [BookStack Logical Theme System](https://www.bookstackapp.com/docs/admin/themes/). Replace `themes/my-theme/` with your actual theme folder.

<table tabindex="0"><thead><tr><th align="left">Component</th><th align="left">Source File</th><th align="left">Destination</th><th align="left">Description</th></tr></thead><tbody><tr><td align="left">**Logic**</td><td align="left">`functions.php`</td><td align="left">`themes/my-theme/functions.php`</td><td align="left">Handles backend interception and PIN validation.</td></tr><tr><td align="left">**Footer**</td><td align="left">`footer.blade.php`</td><td align="left">`themes/my-theme/layouts/parts/footer.blade.php`</td><td align="left">Handles JS visual scrubbing of tags.</td></tr><tr><td align="left">**UI**</td><td align="left">`entity-permissions.blade.php`</td><td align="left">`themes/my-theme/form/entity-permissions.blade.php`</td><td align="left">Adds the lock interface to the permissions page.</td></tr><tr><td align="left">**CLI Tool**</td><td align="left">`ManageLocksCommand.php`</td><td align="left">`themes/my-theme/app/Console/Commands/ManageLocksCommand.php`</td><td align="left">**sudo php artisan bookstack:manage-locks**. Admin tool for managing locks.</td></tr></tbody></table>

### 2. Configuration

<div class="markdown-heading" dir="auto">[<svg aria-hidden="true" class="octicon octicon-link" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg>](https://github.com/Rockyroad1827/Bookstacks-Password-Protect-Page-Hack/tree/main#2-configuration)</div>Open your BookStack `.env` file and add a Master PIN. This is used if a page is locked but no custom password is set.

```
# .env
SECURE_PAGE_PIN=123456
```

<div class="highlight highlight-source-shell notranslate position-relative overflow-auto" dir="auto"><div class="zeroclipboard-container"><svg aria-hidden="true" class="octicon octicon-copy js-clipboard-copy-icon" data-view-component="true" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z"></path><path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"></path></svg></div></div>### 3. Usage: Web Interface

<div class="markdown-heading" dir="auto">[<svg aria-hidden="true" class="octicon octicon-link" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg>](https://github.com/Rockyroad1827/Bookstacks-Password-Protect-Page-Hack/tree/main#3-usage-web-interface)</div>- Navigate to the page you wish to protect.
- Click Permissions in the sidebar.
- Scroll down to the PIN Protection card.
- Enter a Custom Password (optional) and click Enable Lock.

<svg aria-hidden="true" class="octicon octicon-alert mr-2" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="M6.457 1.047c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0 1 14.082 15H1.918a1.75 1.75 0 0 1-1.543-2.575Zm1.763.707a.25.25 0 0 0-.44 0L1.698 13.132a.25.25 0 0 0 .22.368h12.164a.25.25 0 0 0 .22-.368Zm.53 3.996v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"></path></svg>Warning

If you leave the password blank, the page will require the SECURE\_PAGE\_PIN defined in your .env.

The page title will automatically update to include "🔒" to indicate its protected status.

### 4. Parent Deletion

<div class="markdown-heading" dir="auto">[<svg aria-hidden="true" class="octicon octicon-link" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg>](https://github.com/Rockyroad1827/Bookstacks-Password-Protect-Page-Hack/tree/main#4-parent-deletion)</div>Users will be asked to Unlock, Move or delete locked pages before parents can be deleted (Shelf, Book, Chapter). This is to stop abuse of admin permissions if they haven't got access to the Locked file. It also goes under the prosumption that the file is locked for an important reason stopping loss of important data

### 5. Usage: CLI Tool

<div class="markdown-heading" dir="auto">[<svg aria-hidden="true" class="octicon octicon-link" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg>](https://github.com/Rockyroad1827/Bookstacks-Password-Protect-Page-Hack/tree/main#5-usage-cli-tool)</div>You can manage locks directly from the terminal using `php artisan bookstack:manage-locks`. This is useful for auditing protected pages, resetting forgotten PINs, or bulk unlocking content.

How to run it Open your terminal and navigate to your BookStack root directory (e.g., /var/www/bookstack).

Run the script using PHP:

```
php artisan bookstack:manage-locks
```

<div class="highlight highlight-source-shell notranslate position-relative overflow-auto" dir="auto"><div class="zeroclipboard-container"><svg aria-hidden="true" class="octicon octicon-copy js-clipboard-copy-icon" data-view-component="true" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z"></path><path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"></path></svg></div></div><svg aria-hidden="true" class="octicon octicon-info mr-2" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"></path></svg>Note

When you run the command, you will see a list of all currently protected pages:

```
🔒 BookStack Secure Page Manager
================================
ID    | Page Title                               | Current Password    
----------------------------------------------------------------------
[1]   | Server Access Codes 🔒                  | [Master PIN]        
[2]   | HR Confidential 🔒                      | secret123           
----------------------------------------------------------------------
Current .env Master PIN: ****

```

<div class="snippet-clipboard-content notranslate position-relative overflow-auto"><div class="zeroclipboard-container"><svg aria-hidden="true" class="octicon octicon-copy js-clipboard-copy-icon" data-view-component="true" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z"></path><path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"></path></svg></div></div>```
Enter Page ID to edit, "M" to Change Master PIN, or press Enter to exit: 
To Edit a Lock:

Type the ID number (e.g., 2) and press Enter.

```

<div class="snippet-clipboard-content notranslate position-relative overflow-auto"><div class="zeroclipboard-container"><svg aria-hidden="true" class="octicon octicon-copy js-clipboard-copy-icon" data-view-component="true" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z"></path><path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"></path></svg></div></div>The command will show the current status and ask for a new password.

```
Selected: HR Confidential 🔒
Current Password: secret123
Enter NEW password (leave empty to use Master PIN, or type 'DELETE' to unlock):
Options:

```

<div class="snippet-clipboard-content notranslate position-relative overflow-auto"><div class="zeroclipboard-container"><svg aria-hidden="true" class="octicon octicon-copy js-clipboard-copy-icon" data-view-component="true" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z"></path><path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"></path></svg></div></div>Type a new password: Updates the lock to the new code.

Press `Enter` (Empty): Sets the page to use the global Master PIN.

Type `DELETE`: Removes the lock entirely and makes the page public again.

```
To Edit the master pin:

Type "M" and follow the prompts

```

<div class="snippet-clipboard-content notranslate position-relative overflow-auto"><div class="zeroclipboard-container"><svg aria-hidden="true" class="octicon octicon-copy js-clipboard-copy-icon" data-view-component="true" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z"></path><path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"></path></svg></div></div>**Full Changelog**: [https://github.com/Rockyroad1827/Bookstacks-Password-Protect-Page-Hack/commits/Bookstack-Hack](https://github.com/Rockyroad1827/Bookstacks-Password-Protect-Page-Hack/commits/Bookstack-Hack)

</details>## Anforderung

 lightweight, PIN-based protection layer for [BookStack](https://www.bookstackapp.com/). This modification allows administrators to "lock" specific pages behind a custom PIN or a global Master PIN, effectively hiding the content until the correct code is entered.

### Features

- **Per-Page Locking:** Secure specific pages without altering global role permissions.
- **Dual PIN Modes:** Pages can use a specific "Custom Password" or fall back to a global "Master PIN" defined in your `.env` file.
- **Native UI Integration:** Controls are embedded directly into the Page "Permissions" view.
- **Visual Indicators:** Automatically adds a lock symbol (🔒) to page titles and status indicators in the UI.
- **Search &amp; Preview Scrubbing:** Prevents protected content from leaking into search results or list previews by clearing preview text for protected items.
- **CLI Management Tool:** Includes a terminal script to manage locks, update passwords, and remove protections.

## betroffene Dateien

<p class="callout info">Dateien müssen sich in der entsprechenden Struktur unterhalb des Themes befinden.  
Ordner = *kursiv*  
Dateien = **fett**</p>

- **functions.php**
- *layouts*
    - *parts*
        - **footer.blade.php**
- *form*
    - **entity-permissions.blade.php**
- *app*
    - *Console*
        - *Commands*
            - **ManageLocksCommand.php**

## Inhalte der Dateien

<details id="bkmrk-export-menu.blade.ph"><summary>ManageLocksCommand.php</summary>

```php
<?php

namespace BookStack\Console\Commands;

use BookStack\Entities\Models\Page;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Artisan;

class ManageLocksCommand extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'bookstack:manage-locks';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Manage PIN protection on BookStack pages';

    /**
     * Execute the console command.
     */
    public function handle(): int
    {
        $this->info("🔒  BookStack Secure Page Manager");
        $this->info("================================");

        // Fetch Protected Pages
        $protectedPages = Page::whereHas('tags', function ($query) {
            $query->where('name', 'Protected');
        })->get();

        if ($protectedPages->isEmpty()) {
            $this->warn("No protected pages found.");
        }

        // Build Table Data
        $headers = ['ID', 'Page Title', 'Current Password'];
        $rows = [];
        $map = [];

        foreach ($protectedPages as $page) {
            $tag = $page->tags->where('name', 'Protected')->first();
            $currentPass = $tag->value ? $tag->value : "<fg=yellow>[Master PIN]</>";
            
            $rows[] = [
                $page->id,
                $page->name,
                $currentPass
            ];
            
            // Map ID to Page Object for easy lookup
            $map[$page->id] = $page;
        }

        $this->table($headers, $rows);
        $this->line("Current .env Master PIN: <fg=green>" . env('SECURE_PAGE_PIN', '(Not Set)') . "</>");
        $this->line("");

        // Interactive Selection
        $choice = $this->ask('Enter Page ID to edit, "M" to Change Master PIN, or press Enter to exit');

        // --- OPTION M: CHANGE MASTER PIN ---
        if (strtoupper($choice) === 'M') {
            return $this->changeMasterPin($protectedPages);
        }

        // --- OPTION: EXIT ---
        if (!$choice || !isset($map[$choice])) {
            $this->info("Exiting.");
            return 0;
        }

        // --- OPTION: EDIT SINGLE PAGE ---
        $selectedPage = $map[$choice];
        $currentTag = $selectedPage->tags->where('name', 'Protected')->first();
        
        $this->line("");
        $this->info("Selected: <fg=cyan>{$selectedPage->name}</>");
        $this->line("Current Password: " . ($currentTag->value ?: "Master PIN"));

        // Update Logic
        $newPass = $this->ask("Enter NEW password (leave empty for Master PIN, or type 'DELETE' to unlock)");

        if ($newPass === 'DELETE') {
            // Remove the protection tag
            $currentTag->delete();

            // Remove the Lock Symbol from the Page Title
            $lockSymbol = '🔒';
            if (strpos($selectedPage->name, $lockSymbol) !== false) {
                $selectedPage->name = trim(str_replace($lockSymbol, '', $selectedPage->name));
                $selectedPage->save();
                $this->info("✅  Lock symbol removed from page title.");
            }

            $this->info("✅  Protection REMOVED from page.");
        } else {
            $currentTag->value = $newPass;
            $currentTag->save();
            
            $status = empty($newPass) ? "Master PIN" : $newPass;
            $this->info("✅  Password updated to: {$status}");
        }

        return 0;
    }

    /**
     * Handle the Master PIN update logic
     */
    protected function changeMasterPin($allProtectedPages)
    {
        $this->info("\n--- UPDATING MASTER PIN ---");
        
        // Capture the OLD Master PIN (from loaded env) before we change it
        $oldMasterPin = env('SECURE_PAGE_PIN');
        
        $newPin = $this->ask("Enter the NEW Master PIN");

        if (empty($newPin)) {
            $this->error("PIN cannot be empty.");
            return 1;
        }

        if (!$this->confirm("Are you sure you want to change the Master PIN to '{$newPin}'?")) {
            $this->info("Operation cancelled.");
            return 0;
        }

        // Update .env File
        $envPath = base_path('.env');
        if (File::exists($envPath)) {
            $content = File::get($envPath);
            $key = 'SECURE_PAGE_PIN';
            
            if (strpos($content, "$key=") !== false) {
                // Replace existing key
                $content = preg_replace("/^{$key}=.*/m", "{$key}={$newPin}", $content);
            } else {
                // Append new key
                $content .= "\n{$key}={$newPin}";
            }
            
            File::put($envPath, $content);
            $this->info("✅  Updated .env file.");
        } else {
            $this->error("Could not find .env file at: $envPath");
            return 1;
        }

        // Update Relevant Pages (Explicit + Implicit)
        $explicitUpdates = 0;
        $totalUpdated = 0;
        
        foreach ($allProtectedPages as $page) {
            $tag = $page->tags()->where('name', 'Protected')->first();
            if ($tag) {
                $val = $tag->value;

                // Explicitly using the OLD Master PIN -> Update DB to New PIN
                if (!empty($val) && $val == $oldMasterPin) {
                    $tag->value = $newPin; 
                    $tag->save();
                    $explicitUpdates++;
                    $totalUpdated++;
                }
                // Implicitly using Master PIN (empty value) -> Inherits New PIN automatically
                elseif (empty($val)) {
                    $totalUpdated++;
                }
                // Custom Password -> Ignored
            }
        }
        
        if ($explicitUpdates > 0) {
            $this->comment("ℹ️  Database updated for {$explicitUpdates} pages that explicitly used the old PIN.");
        }

        // Clear Config Cache
        $this->call('config:clear');
        
        // Final Summary
        $this->info("✅  Updated {$totalUpdated} total pages.");

        return 0;
    }
}
```

</details><details id="bkmrk-revisions-index-row-"><summary>functions.php</summary>

```php
<?php
use BookStack\Entities\Models\Page;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\View;
use Illuminate\Support\Facades\Event;
use Illuminate\Routing\Events\RouteMatched;

// --------------------------------------------------------------------------------
// Page Lock SEARCH BLOCKER (Raw Request Intercept)
// --------------------------------------------------------------------------------
// Immediately blocks search requests for "Protected" tags to prevent leaks.
if (isset($_SERVER['REQUEST_URI'])) {
    $rawUri = $_SERVER['REQUEST_URI'];
    $decodedUri = rawurldecode($rawUri);
    if (strpos($rawUri, '/search') !== false && stripos($decodedUri, 'Protected') !== false) {
        header("Location: /");
        exit();
    }
}

///////////////////////////////////////////////////////////////////////////////////
// Page Lock Logic
///////////////////////////////////////////////////////////////////////////////////

// --------------------------------------------------------------------------------
// HELPER: CHECK IF SPECIFIC PAGE IS UNLOCKED
// --------------------------------------------------------------------------------
if (!function_exists('isPageUnlocked')) {
    function isPageUnlocked($pageId) {
        // Retrieve the list of unlocked pages: [ PageID => Timestamp ]
        $unlockedPages = Session::get('secure_unlocked_pages', []);
        
        // Check if this ID exists and the timestamp is in the future
        if (isset($unlockedPages[$pageId]) && $unlockedPages[$pageId] > time()) {
            return true;
        }
        return false;
    }
}

// --------------------------------------------------------------------------------
// HELPER: GET REQUIRED PIN
// --------------------------------------------------------------------------------
if (!function_exists('getSecurePagePin')) {
    function getSecurePagePin($page) {
        $tag = $page->tags()->where('name', 'Protected')->first();
        if (!$tag) return null;
        return !empty($tag->value) ? $tag->value : env('SECURE_PAGE_PIN');
    }
}

// --------------------------------------------------------------------------------
// EXPORT INTERCEPTOR (PDF, HTML, TXT, MD, ZIP)
// --------------------------------------------------------------------------------
// Listens for ANY route that matches. If the URL contains '/export/', we verify access.
Event::listen(RouteMatched::class, function (RouteMatched $event) {
    $request = request();
    $path = $request->path();

    // Check if this is an export URL
    if (strpos($path, '/export/') !== false) {
        
        // Try to find the Page Slug in the route parameters
        // BookStack usually names this parameter 'pageSlug' or 'page'
        $slug = $event->route->parameter('pageSlug');

        if ($slug) {
            // Find the page in the DB
            $page = Page::where('slug', $slug)->first();

            // Check if Page exists, is Protected, and is NOT unlocked
            if ($page && $page->tags()->where('name', 'Protected')->exists()) {
                if (!isPageUnlocked($page->id)) {
                    
                    // Block Download & Redirect to Lock Screen
                    // We attach the CURRENT export URL as the "redirect_after_unlock" target.
                    // Once unlocked, the user will be bounced right back here to start the download.
                    $currentExportUrl = $request->fullUrl();
                    $lockScreenUrl = $page->getUrl() . '?redirect_after_unlock=' . urlencode($currentExportUrl);
                    
                    header("Location: " . $lockScreenUrl);
                    exit();
                }
            }
        }
    }
});

// --------------------------------------------------------------------------------
// BACKEND ROUTES (PIN LOGIC)
// --------------------------------------------------------------------------------
if (!app()->routesAreCached()) {
    // Check PIN
    Route::post('/secure-pin-check', function () {
        $input = request()->input('pin_code');
        $pageId = request()->input('page_id');
        $redirect = request()->input('redirect_to', '/');
        
        $targetPin = env('SECURE_PAGE_PIN');
        if ($pageId) {
            $page = Page::find($pageId);
            if ($page) {
                $targetPin = getSecurePagePin($page);
            }
        }

        if ($input && $targetPin && (string)$input === (string)$targetPin) {
            $unlockedPages = Session::get('secure_unlocked_pages', []);
            $unlockedPages[$pageId] = time() + 5; 
            Session::put('secure_unlocked_pages', $unlockedPages);
            Session::save();
            
            return redirect($redirect);
        }
        return redirect($redirect)->with('pin_error', 'Invalid Access Code');
    })->middleware('web');

    // Enable Lock & Add Emoji
    Route::post('/secure-lock-page', function () {
        $pageId = request()->input('page_id');
        $customPass = request()->input('custom_password');
        $redirectUrl = request()->input('redirect_to', '/');
        $page = Page::find($pageId);

        if ($page && userCan('page-update', $page)) {
            // 1. Add Tag
            if (!$page->tags()->where('name', 'Protected')->exists()) {
                $page->tags()->create(['name' => 'Protected', 'value' => $customPass]);
                
                // 2. Add Lock Emoji to Title
                if (strpos($page->name, '🔒') === false) {
                    $page->name = trim($page->name) . ' 🔒';
                    $page->save();
                }

                Session::flash('success', 'PIN protection enabled.');
            }
        }
        return redirect($redirectUrl);
    })->middleware('web');

    // Disable Lock & Remove Emoji
    Route::post('/secure-unlock-page', function () {
        $pageId = request()->input('page_id');
        $redirectUrl = request()->input('redirect_to', '/');
        $page = Page::find($pageId);

        if ($page && userCan('page-update', $page)) {
            $tag = $page->tags()->where('name', 'Protected')->first();
            if ($tag) {
                // 1. Remove Tag
                $tag->delete();

                // 2. Remove Lock Emoji from Title
                if (strpos($page->name, '🔒') !== false) {
                    $page->name = trim(str_replace('🔒', '', $page->name));
                    $page->save();
                }

                Session::flash('success', 'PIN protection removed.');
            }
        }
        return redirect($redirectUrl);
    })->middleware('web');
}

// --------------------------------------------------------------------------------
// LOCK SCREEN GENERATOR
// --------------------------------------------------------------------------------
if (!function_exists('renderSecureLockScreen')) {
    function renderSecureLockScreen($title = 'Protected Content', $pageId = null) {
        $errorHtml = Session::has('pin_error') ? 
            '<div class="text-neg bold mb-m" style="background: #ffebeb; border: 1px solid #cb2431; padding: 10px; border-radius: 4px;">' . Session::get('pin_error') . '</div>' : '';
        $pageIdInput = $pageId ? '<input type="hidden" name="page_id" value="' . $pageId . '">' : '';

        // Detect Redirect Intent
        $targetRedirect = request()->get('redirect_after_unlock');
        if (!$targetRedirect) {
            $targetRedirect = request()->fullUrl();
        }

        return '
        <div class="flex-fill flex-container-column justify-center items-center" style="min-height: 60vh;">
            <div class="card content-wrap auto-height" style="max-width: 500px; width: 100%; box-shadow: 0 4px 12px rgba(0,0,0,0.1);">
                <div class="text-center mb-l">
                    <div style="width: 80px; height: 80px; background-color: var(--color-primary); color: #FFF; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; margin-bottom: 20px; box-shadow: 0 4px 6px rgba(0,0,0,0.1);">
                         <svg fill="currentColor" width="40" height="40" viewBox="0 0 24 24"><path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2z"/></svg>
                    </div>
                    <h1 class="list-heading text-xl mb-s">' . $title . '</h1>
                    <p class="text-muted">This page is password protected.</p>
                </div>
                ' . $errorHtml . '
                <form method="POST" action="/secure-pin-check" class="stretch-inputs">
                    <input type="hidden" name="_token" value="' . csrf_token() . '">
                    ' . $pageIdInput . '
                    <input type="hidden" name="redirect_to" value="' . htmlspecialchars($targetRedirect) . '">
                    <div class="form-group mb-l">
                        <label for="pin_code" class="text-muted mb-xs">Access Code</label>
                        <input type="password" id="pin_code" name="pin_code" placeholder="Enter PIN..." class="input-base" style="font-size: 1.1em; padding: 10px;" autofocus>
                    </div>
                    <div class="form-group text-right">
                         <a href="/" class="button outline mr-m">Cancel</a>
                        <button type="submit" class="button primary">Unlock Page</button>
                    </div>
                </form>
            </div>
        </div>
        <style>.tri-layout-right, .tri-layout-left, .toolbar { display: none !important; } #main-content { width: 100% !important; margin: 0 !important; max-width: 100% !important; }</style>';
    }
}

// --------------------------------------------------------------------------------
// VIEW INTERCEPTOR: SHOW PAGE (Main Content)
// --------------------------------------------------------------------------------
View::composer(['pages.show'], function ($view) {
    $data = $view->getData();
    if (!isset($data['page'])) return;
    $page = $data['page'];
    
    if ($page->tags()->where('name', 'Protected')->exists() && !isPageUnlocked($page->id)) {
        $page->html = renderSecureLockScreen("Protected Content", $page->id);
    }
});

// --------------------------------------------------------------------------------
// VIEW INTERCEPTOR: ACTIONS (Edit, Copy, Move, etc.)
// --------------------------------------------------------------------------------
View::composer([
    'pages.edit', 'pages.move', 'pages.revisions', 'pages.delete', 'pages.copy', 'form.entity-permissions'
], function ($view) {
    $data = $view->getData();
    $page = $data['page'] ?? $data['model'] ?? null;

    if ($page instanceof \BookStack\Entities\Models\Page) {
         if ($page->tags()->where('name', 'Protected')->exists() && !isPageUnlocked($page->id)) {
             $currentActionUrl = request()->fullUrl();
             $redirectUrl = $page->getUrl() . '?redirect_after_unlock=' . urlencode($currentActionUrl);
             header("Location: " . $redirectUrl);
             exit();
         }
    }
});

// --------------------------------------------------------------------------------
// LIST VIEW SCRUBBER
// --------------------------------------------------------------------------------
View::composer([
    'partials.entity-list-item', 'partials.page-list-item', 'partials.book-content-list-item'
], function ($view) {
    $data = $view->getData();
    $entity = $data['entity'] ?? $data['page'] ?? null;

    if ($entity && isset($entity->tags)) {
        $hasProtectedTag = $entity->tags->contains(function($tag) {
            return strtolower($tag->name) === 'protected';
        });

        if ($hasProtectedTag) {
            $entity->text = '';
            $entity->html = '';
            $entity->preview_html = '';
        }

        $entity->tags = $entity->tags->filter(function($tag) {
            return strtolower($tag->name) !== 'protected';
        });
    }
});

// --------------------------------------------------------------------------------
// REGISTER CUSTOM ARTISAN COMMAND
// --------------------------------------------------------------------------------
if (app()->runningInConsole()) {
    // 1. Manually load the class file
    $commandFile = __DIR__ . '/app/Console/Commands/ManageLocksCommand.php';
    
    if (file_exists($commandFile)) {
        require_once $commandFile;

        // 2. Register the command with Laravel
        // FIX: Use the concrete Application class instead of the Artisan facade
        \Illuminate\Console\Application::starting(function ($artisan) {
            $artisan->resolveCommands([\BookStack\Console\Commands\ManageLocksCommand::class]);
        });
    }
}
// --------------------------------------------------------------------------------
// PARENT DELETION BLOCKER (Shelves, Books, Chapters)
// --------------------------------------------------------------------------------
// Prevents deletion if the entity contains any "Protected" pages.
View::composer(['shelves.delete', 'books.delete', 'chapters.delete'], function ($view) {
    $data = $view->getData();
    $entity = null;
    $protectedCount = 0;

    // Identify Entity Type and Count Protected Children
    if (isset($data['shelf'])) {
        $entity = $data['shelf'];
        foreach ($entity->books as $book) {
            $protectedCount += $book->pages()->whereHas('tags', function($q){
                $q->where('name', 'Protected');
            })->count();
        }
    } elseif (isset($data['book'])) {
        $entity = $data['book'];
        $protectedCount = $entity->pages()->whereHas('tags', function($q){
            $q->where('name', 'Protected');
        })->count();
    } elseif (isset($data['chapter'])) {
        $entity = $data['chapter'];
        $protectedCount = $entity->pages()->whereHas('tags', function($q){
            $q->where('name', 'Protected');
        })->count();
    }

    // If protected content is found, block access
    if ($entity && $protectedCount > 0) {
        // FLASH A SPECIAL SESSION KEY TO TRIGGER THE POPUP
        Session::flash('protected_deletion_blocked', $protectedCount);
        
        // CRITICAL FIX: Force Laravel to write the session before we exit
        Session::save();
        
        // Redirect back to the entity page
        header("Location: " . $entity->getUrl());
        exit();
    }
});
```

</details><details id="bkmrk-footer.blade.php-%3Cdi"><summary>footer.blade.php</summary>

```php
{{-- DELETION BLOCKED MODAL --}}
@if(session()->has('protected_deletion_blocked'))
    <div id="lock-block-modal" style="display: flex; align-items: center; justify-content: center; position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 9999; background-color: rgba(0,0,0,0.6); backdrop-filter: blur(2px);">
        <div class="card p-xl" style="max-width: 450px; width: 90%; border-radius: 8px; box-shadow: 0 10px 25px rgba(0,0,0,0.2);">
            <div class="text-center">
                <div class="text-neg mb-l" style="display: inline-flex; padding: 16px; background-color: #fff2f2; border-radius: 50%;">
                    <svg fill="currentColor" width="48" height="48" viewBox="0 0 24 24"><path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2z"/></svg>
                </div>
                <h3 class="list-heading text-xl bold mb-s">Action Blocked</h3>
                <p class="text-muted mb-l" style="font-size: 1.1em; line-height: 1.5;">
                    This item contains <strong>{{ session('protected_deletion_blocked') }}</strong> PIN-protected page(s).<br>
                    <span class="small">You must unlock or move these pages before this item can be deleted.</span>
                </p>
                {{-- REMOVED inline onclick, ADDED ID --}}
                <button type="button" id="close-lock-modal" class="button primary">Understood</button>
            </div>
        </div>
    </div>
@endif

<div class="print-hidden">
    <footer class="px-xl py-m mt-xl border-top text-muted text-small">
        <div class="container">
        </div>
    </footer>

    <script nonce="{{ $cspNonce }}">
    (function() {
        console.log("Authorized Footer Script Running");

        // SETUP MODAL CLOSING LOGIC
        const closeBtn = document.getElementById('close-lock-modal');
        if (closeBtn) {
            closeBtn.addEventListener('click', function() {
                const modal = document.getElementById('lock-block-modal');
                if (modal) modal.remove();
            });
        }

        // HIDE TAGS LOGIC
        function hideProtectedTags() {
            const tags = document.querySelectorAll('.tag-item');
            
            tags.forEach(tag => {
                if (tag.dataset.protectedChecked) return;

                const tagText = tag.innerText.trim();
                if (tagText.startsWith('Protected')) {
                    tag.style.display = 'none';
                    tag.dataset.protectedHidden = "true";
                }
                tag.dataset.protectedChecked = "true";
            });
        }

        // HIDE CONTENT LOGIC
        function hideProtectedContent() {
            const trigger = "🔒"; 
            const items = document.querySelectorAll('.entity-list-item');

            items.forEach(item => {
                const titleElement = item.querySelector('.entity-list-item-name');
                
                if (titleElement && titleElement.textContent.includes(trigger)) {
                    
                    const desc = item.querySelector('.entity-list-item-desc');
                    if (desc) desc.style.display = 'none';

                    const snippet = item.querySelector('.entity-list-item-snippet'); 
                    if (snippet) {
                        snippet.style.display = 'none';
                    } else {
                        const mutedItems = item.querySelectorAll('.text-muted');
                        mutedItems.forEach(el => {
                            if (el.querySelector('a') === null) { 
                                el.style.display = 'none';
                            }
                        });
                    }
                }
            });
        }

        // EXECUTION
        hideProtectedTags();
        hideProtectedContent();

        document.addEventListener("DOMContentLoaded", function() {
            hideProtectedTags();
            hideProtectedContent();
        });

        const observer = new MutationObserver(function(mutations) {
            hideProtectedTags();
            hideProtectedContent();
        });

        observer.observe(document.body, {
            childList: true,
            subtree: true
        });
    })();
    </script>
</div>

```

</details><details id="bkmrk-entity-permissions.b-1"><summary>entity-permissions.blade.php</summary>

```php
<?php
/** @var \BookStack\Permissions\PermissionFormData $data */

// --- CHECK STATUS ---
$isProtected = false;
$hasCustomPass = false;

if($model instanceof \BookStack\Entities\Models\Page) {
    // Check for the tag
    $tag = $model->tags()->where('name', 'Protected')->first();
    $isProtected = !is_null($tag);
    $hasCustomPass = $isProtected && !empty($tag->value);
}
?>

{{-- MAIN PERMISSIONS FORM --}}
<form component="entity-permissions"
      option:entity-permissions:entity-type="{{ $model->getType() }}"
      action="{{ $model->getUrl('/permissions') }}"
      method="POST"
      id="main-permissions-form">
    {!! csrf_field() !!}
    <input type="hidden" name="_method" value="PUT">

    <div class="grid half left-focus v-end gap-m wrap">
        <div>
            <h1 class="list-heading">{{ $title }}</h1>
            <p class="text-muted mb-s">
                {{ trans('entities.permissions_desc') }}
                @if($model instanceof \BookStack\Entities\Models\Book)
                    <br> {{ trans('entities.permissions_book_cascade') }}
                @elseif($model instanceof \BookStack\Entities\Models\Chapter)
                    <br> {{ trans('entities.permissions_chapter_cascade') }}
                @endif
            </p>
        </div>
    </div>

    {{-- WARNINGS --}}
    @if($model instanceof \BookStack\Entities\Models\Bookshelf)
        <p class="text-warn">{{ trans('entities.shelves_permissions_cascade_warning') }}</p>
    @endif

    <div class="flex-container-row justify-flex-end">
        <div class="form-group mb-m">
            <label for="owner">{{ trans('entities.permissions_owner') }}</label>
            @include('form.user-select', ['user' => $model->ownedBy, 'name' => 'owned_by'])
        </div>
    </div>

    <hr>

    {{-- ROLE PERMISSIONS LIST --}}
    <div refs="entity-permissions@role-container" class="item-list mt-m mb-m">
        @foreach($data->permissionsWithRoles() as $permission)
            @include('form.entity-permissions-row', [
                'permission' => $permission,
                'role' => $permission->role,
                'entityType' => $model->getType(),
                'inheriting' => false,
            ])
        @endforeach
    </div>

    <div class="flex-container-row justify-flex-end mb-xl">
        <div class="flex-container-row items-center gap-m">
            <label for="role_select" class="m-none p-none"><span
                        class="bold">{{ trans('entities.permissions_role_override') }}</span></label>
            <select name="role_select" id="role_select" refs="entity-permissions@role-select">
                <option value="">{{ trans('common.select') }}</option>
                @foreach($data->rolesNotAssigned() as $role)
                    <option value="{{ $role->id }}">{{ $role->display_name }}</option>
                @endforeach
            </select>
        </div>
    </div>

    <div class="item-list mt-m mb-xl">
        @include('form.entity-permissions-row', [
                'role' => $data->everyoneElseRole(),
                'permission' => $data->everyoneElseEntityPermission(),
                'entityType' => $model->getType(),
                'inheriting' => !$model->permissions()->where('role_id', '=', 0)->exists(),
            ])
    </div>

    {{-- NATIVE-STYLE PIN CONTROL --}}
    @if($model instanceof \BookStack\Entities\Models\Page)
        <div class="mb-m mt-l">
            <div class="card p-m" style="border: 1px solid #ddd; box-shadow: 0 1px 3px rgba(0,0,0,0.05);">
                <div class="flex-container-row justify-space-between items-center wrap gap-m">
                    
                    {{-- Left Side: Status Text --}}
                    <div style="flex: 1;">
                        <div class="flex-container-row items-center gap-xs">
                            @if($isProtected)
                                <span class="text-neg bold">@icon('lock') PIN Protection Active</span>
                            @else
                                <span class="text-pos bold">@icon('lock-open') PIN Protection Inactive</span>
                            @endif
                        </div>
                        <p class="text-muted small mb-none mt-xs">
                            @if($isProtected)
                                This page is locked. 
                                @if($hasCustomPass)
                                    (Using <strong>Custom</strong> Password)
                                @else
                                    (Using <strong>Master</strong> PIN)
                                @endif
                            @else
                                Restrict access to this page with a password.
                            @endif
                        </p>
                    </div>
                    
                    {{-- Right Side: Controls --}}
                    <div>
                        @if($isProtected)
                            {{-- UNLOCK BUTTON --}}
                            <div style="text-align: right;">
                                <button type="submit" form="form-secure-unlock" class="button outline small" style="color: #c0392b; border-color: #c0392b;">
                                    @icon('close') Disable Lock
                                </button>
                            </div>
                        @else
                            {{-- LOCK FORM with Input --}}
                            <div class="flex-container-row gap-s items-center">
                                <input type="text" name="custom_password" form="form-secure-lock" placeholder="Custom Password (Optional)" class="input-base small" style="width: 200px; margin:0;" autocomplete="off">
                                <button type="submit" form="form-secure-lock" class="button small" style="background-color: #27ae60; border-color: #27ae60; color: #fff;">
                                    @icon('lock') Enable Lock
                                </button>
                            </div>
                        @endif
                    </div>

                </div>
            </div>
        </div>
    @endif
    {{-- END PIN CONTROL --}}

    <hr class="mb-m">

    <div class="flex-container-row justify-space-between gap-m wrap">
        <div class="flex min-width-m">
            @if($model instanceof \BookStack\Entities\Models\Bookshelf)
                <p class="small text-muted mb-none">
                    * {{ trans('entities.shelves_permissions_create') }}
                </p>
            @endif
        </div>
        <div class="text-right">
            <a href="{{ $model->getUrl() }}" class="button outline">{{ trans('common.cancel') }}</a>
            <button type="submit" class="button">{{ trans('entities.permissions_save') }}</button>
        </div>
    </div>
</form>

{{-- 
    HIDDEN FORMS
--}}
@if($model instanceof \BookStack\Entities\Models\Page)
    <form id="form-secure-lock" action="/secure-lock-page" method="POST" style="display: none;">
        {!! csrf_field() !!}
        <input type="hidden" name="page_id" value="{{ $model->id }}">
        {{-- Redirect back to this permissions page --}}
        <input type="hidden" name="redirect_to" value="{{ url()->current() }}">
    </form>

    <form id="form-secure-unlock" action="/secure-unlock-page" method="POST" style="display: none;">
        {!! csrf_field() !!}
        <input type="hidden" name="page_id" value="{{ $model->id }}">
        <input type="hidden" name="redirect_to" value="{{ url()->current() }}">
    </form>
@endif
```

</details>## Screenshots

# Anzeige von Buch, Kapitel und Seitentitel im Footer

<p class="callout success">getestet mit Version **25.12.8**</p>

## Anforderung

Zur besseren Nachverfolgung von Exports an welcher Stelle die exportierte Seite im BookStack zu finden sind.

## betroffene Dateien

<p class="callout info">Dateien müssen sich in der entsprechenden Struktur unterhalb des Themes befinden.  
Ordner = *kursiv*  
Dateien = **fett**</p>

- *layouts*
    - *parts*
        - **export-body-start.blade.php**

## Inhalte der Dateien

<details id="bkmrk-export-menu.blade.ph"><summary>export-body-start.blade.php</summary>

Hier wird folgender Bereich benötigt wenn nicht schon vorhanden bzw. muss im den Teil ergänzt werden wenn schon vorhanden:

```html
[...]
<div style="float: left; opacity: 0.8; font-size: 8pt; text-align: center">
    <!-- Anzeige von Buch, Kapitel und Seitentitel im Footer -->
    @if(isset($page))
        {{-- Buch & optional Kapitel & Seite --}}
        {{ trans('entities.book') }}: {{ $page->book->name ?? '' }}
        @if(isset($page->chapter))
            – {{ trans('entities.chapter') }}: {{ $page->chapter->name }}
        @endif
        <br>
        {{ trans('entities.pages_title') }}: {{ $page->name }}
    @elseif(isset($chapter))
        {{-- Export eines Kapitels --}}
        {{ trans('entities.book') }}: {{ $chapter->book->name ?? '' }}<br>
        {{ trans('entities.chapter') }}: {{ $chapter->name }}
    @elseif(isset($book))
        {{-- Export eines ganzen Buchs --}}
        {{ trans('entities.book') }}: {{ $book->name }}
    @endif
</div>
[...]
```

</details>## Screenshots

[![image.png](https://bookstack.jelinek-rz.de/uploads/images/gallery/2026-03/scaled-1680-/image.png)](https://bookstack.jelinek-rz.de/uploads/images/gallery/2026-03/image.png)