Initial Commit

This commit is contained in:
2026-09-04 17:36:50 +02:00
parent bba465bf5a
commit 7ce9248e95
2 changed files with 480 additions and 15 deletions

29
LICENSE
View File

@@ -1,18 +1,21 @@
MIT License
Copyright (c) 2026 marco.morath
Copyright (c) 2026 Marco Morath
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
associated documentation files (the "Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
following conditions:
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial
portions of the Software.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
USE OR OTHER DEALINGS IN THE SOFTWARE.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

466
README.md
View File

@@ -1,3 +1,465 @@
# libreoffice-paperless-connector
# Paperless-Connector für LibreOffice
Addon für Libreoffice um Paperless-NGX anzubinden.
Erweiterung, die Dokumente direkt aus einem Paperless-ngx-Archiv öffnet,
Bearbeitungen als **neue Version desselben Dokuments** ablegt, die
Klassifizierung bearbeitet und zeitlich begrenzte Freigabelinks erzeugt.
Voraussetzung ist Paperless-ngx **3.0 oder neuer** — erst dort gibt es die
Dokumentversionierung.
---
## Funktionen
| Befehl | Wirkung |
|---|---|
| **Aus Paperless öffnen** | Suche, Trefferliste, Versionsliste — lädt das **Original** |
| **Als neue Version ablegen** | hängt die Fassung als Version an das bestehende Dokument |
| **In Paperless ablegen** | neues Dokument **oder** neue Version eines beliebigen anderen Dokuments |
| **Klassifizierung bearbeiten** | Titel, Datum, Korrespondent, Dokumenttyp, Tags |
| **Freigabelink erzeugen** | zeitlich begrenzter, unauthentifizierter Link, optional mit Kennzeichnung |
| **Status anzeigen** | Kennung, Titel, Versionsanzahl des aktuellen Dokuments |
| **Verbindung einrichten** | Basisadresse und API-Token |
Erreichbar über das Menü **Extras → Paperless** und über eine eigene
Symbolleiste.
---
## Sprachen
Die Oberfläche liegt in **Deutsch, Englisch und Französisch** vor. Die
Sprache richtet sich nach der Einstellung von LibreOffice, nicht nach der
des Betriebssystems — maßgeblich ist die Sprache, in der die Erweiterung
erscheint.
Übersetzt wird in `pythonpath/paperless_i18n.py`. Als Schlüssel dient der
deutsche Text selbst; fehlt eine Übersetzung, erscheint das deutsche
Original. Eine weitere Sprache ergänzt man, indem man ein Wörterbuch nach
dem Muster von `EN` anlegt und in `KATALOG` einträgt.
Die Menü- und Symbolleistentexte liegen getrennt davon in `Addons.xcu`;
dort sind sie je Sprache als `xml:lang`-Werte hinterlegt.
## Sicherheit
Ausführlich in [SICHERHEIT.md](SICHERHEIT.md). Die wichtigsten Punkte:
- **Makros werden nie ausgeführt.** Heruntergeladene Dateien werden mit
`MacroExecutionMode = NEVER_EXECUTE` geladen — auch eine Rückfrage gibt
es nicht. Ein Dokument aus dem Archiv ist fremder Inhalt, selbst aus dem
eigenen Bestand.
- **Keine externen Inhalte beim Öffnen.** `UpdateDocMode = NO_UPDATE`.
- **Dateinamen aus der Antwort werden vollständig bereinigt**, beide
Pfadtrennzeichen, Windows-Gerätenamen, Punkte am Ende.
- **Arbeitsverzeichnis im Benutzerprofil**, nicht in `/tmp` — dort wäre ein
Symlink-Angriff möglich.
- **Nur http und https** als Basisadresse und bei Weiterleitungen.
- **Größenbegrenzung** auf Antworten.
## Bewusste Entwurfsentscheidungen
### Eigene Schaltflächen statt Speichern-Knopf
Paperless als echtes Speicherziel in die Datei-Dialoge einzuhängen würde
einen **UCB Content Provider** erfordern — C++-UNO-Arbeit in einer sehr
dünn dokumentierten Ecke, für ein Einzelprojekt nicht tragfähig.
Eigene Schaltflächen sind ohnehin die ehrlichere Lösung: Es bleibt
sichtbar, wann eine neue Version im Archiv entsteht, und ein versehentliches
Strg+S erzeugt keine Archivversion.
### Prüfungen vor der Freigabe
Ein Link auf einen Stand, der nur lokal existiert, wäre irreführend — der
Empfänger bekäme etwas anderes zu sehen als der Absender vor sich hat.
Deshalb wird vorher geprüft:
| Zustand | Reaktion |
|---|---|
| Dokument nicht in Paperless | Angebot, es zuerst abzulegen |
| lokale Änderungen nicht abgelegt | Auswahl: abbrechen, trotzdem teilen, oder erst neue Version ablegen |
Zusätzlich lässt sich das Dokument beim Teilen mit einem **Tag**
kennzeichnen, voreingestellt `Sharelink`. Das macht geteilte Dokumente
später gezielt auffindbar — Paperless zeigt zwar die eigenen Freigaben an,
aber ein Tag lässt sich in gespeicherten Ansichten und Regeln verwenden.
Der zuletzt verwendete Tagname wird gemerkt. Ist er noch nicht vorhanden,
wird er nach Rückfrage angelegt. Vorhandene Tags des Dokuments bleiben
erhalten.
### Drei Wege ins Archiv
| Befehl | Ergebnis |
|---|---|
| **Als neue Version ablegen** | eine weitere Fassung **desselben** Dokuments |
| **In Paperless ablegen** → Neues Dokument | eigenständiges Dokument mit eigener Versionskette und Seriennummer |
| **In Paperless ablegen** → An bestehendes anhängen | neue Version eines **beliebigen anderen** Dokuments |
Der dritte Weg deckt eine Lücke, die Dokumentenverwaltungen gern
offenlassen:
> Die Rechnung kommt als PDF ins Archiv. Später entsteht die bearbeitbare
> Fassung. Beides gehört zusammen — aber das PDF war zuerst da.
„Als neue Version ablegen" zielt immer auf das eigene Wurzeldokument und
hilft hier nicht. Über die Zielauswahl lässt sich dagegen jedes vorhandene
Dokument suchen und die geöffnete Datei daran anhängen.
Beim neuen Dokument lässt sich die Klassifizierung des Ursprungs
übernehmen — Dokumenttyp, Korrespondent, Tags, Zusatzfelder und
Eigentümer. Zusatzfelder mit **Herkunftsbezug** werden bewusst nicht
kopiert — wer aus einem anderen System migriert hat, führt dort oft ein Feld
mit der Fremdkennung, und eine Kopie mit derselben Kennung wäre falsch.
Welche Feldnamen das betrifft, steht in den Einstellungen unter
`herkunftsfelder`.
In beiden Fällen kann das geöffnete Fenster danach dem Ziel zugeordnet
werden. Weitere Versionen landen dann dort.
Der Befehl funktioniert auch mit Dateien, die **nicht** aus Paperless
stammen — dann ist er der Weg, sie überhaupt erst einzuliefern.
Und er funktioniert mit Dokumenten, die **noch nie gespeichert** wurden.
Ein frisch angelegtes „Unbenannt 1" hat keinen Speicherort; es wird dann in
dasselbe temporäre Verzeichnis geschrieben, das auch beim Öffnen benutzt
wird, im zur Anwendung passenden ODF-Format. Der eingegebene Titel bestimmt
dabei den Dateinamen, unter dem Paperless das Original ablegt.
### Ein Dialog von oben nach unten
```
┌─ Suche ──────────────────────────────────────── [Suchen] ─┐
│ │
│ Dokumente ASN Datum Titel │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ 11263 2024-02-11 Mietvertrag │ │
│ │ 11264 2024-03-02 Nebenkostenabrechnung │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ Versionen Nr. Datum Format Bezeichnung │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ • 3 2026-09-01 PDF LibreOffice 2026-09-01 │ │
│ │ 2 2025-06-04 ODF Text │ │
│ │ 1 2024-02-11 ODF Text │ │
│ └──────────────────────────────────────────────────────┘ │
│ Bearbeitbar. Änderungen können als neue Version … │
│ ☐ Schreibgeschützt öffnen [Öffnen] [Abbrechen] │
└────────────────────────────────────────────────────────────┘
```
Die Spaltenüberschrift steht jeweils **unter** der Bezeichnung des
Abschnitts und über der Liste, nicht daneben.
Beide Listen verwenden eine dicht laufende Schrift mit festen
Spaltenbreiten und senkrechten Strichen als Trenner. Die Schriftgröße wird
bewusst nicht festgelegt, sondern vom Dialog geerbt — also die Vorgabe des
Systems. Mit der sonst
üblichen Proportionalschrift wären mit Leerzeichen aufgefüllte Spalten
**nicht** ausgerichtet — die Überschrift stünde woanders als der Inhalt.
Die Trefferliste zeigt je Dokument **einen** Eintrag, nämlich das
Wurzeldokument. Bei Auswahl füllt sich darunter die Versionsliste mit
Nummer, Datum, **Dateiformat** und Bezeichnung.
Vorausgewählt ist die aktuellste Version. Ein **Punkt** markiert Fassungen,
die LibreOffice nach der Formattabelle nicht bearbeiten kann — etwa ein
PDF, während eine ältere Version noch das ODT ist.
### Öffnungsart
Unten im Dialog wird die Öffnungsart gewählt. Die Vorbelegung ist ein
**Vorschlag, keine Vorschrift** — die Formaterkennung kann danebenliegen,
und dann soll der Anwender trotzdem bearbeitbar öffnen können.
| Fall | Vorbelegung | Hinweis |
|---|---|---|
| bekannt und bearbeitbar | Bearbeitbar | — |
| **nicht erkannt** | Bearbeitbar | zeigt Dateiname und MIME-Typ, damit das Format gemeldet und ergänzt werden kann |
| bekannt, nicht bearbeitbar (PDF, E-Mail) | Schreibgeschützt | bearbeitbar bleibt wählbar |
Beim Zurückschreiben eines unbekannten Formats kommt eine Rückfrage: Es
gibt dann keinen passenden Speicherfilter, LibreOffice speichert im
eigenen Format, und die Dateiendung passt danach möglicherweise nicht mehr
zum Inhalt.
Welche Formate als bearbeitbar gelten, steht in
`pythonpath/paperless_formats.py`. Die Tabelle lässt sich dort ergänzen —
wenn im Hinweistext regelmäßig derselbe MIME-Typ auftaucht, gehört er
hinein.
Die Formatangabe steht nicht in der eingebetteten `versions`-Liste, deshalb
ruft die Erweiterung je Version einen Datensatz ab. Die Ergebnisse werden
zwischengespeichert, damit das Blättern in der Trefferliste nicht bei jedem
Tastendruck erneut abfragt.
### Immer das Original laden
Der Download erfolgt mit `?original=true`. Bei ZUGFeRD bleibt damit die
eingebettete XML erhalten, bei E-Mails die Anhänge. Die von Paperless
erzeugte Archivfassung wäre eine Ableitung.
### Kennung in den Dokumenteigenschaften
Die Paperless-ID wird in den **benutzerdefinierten Dokumenteigenschaften**
abgelegt (`PaperlessDocumentId`). Diese überleben einen ODF-Speichervorgang
und wandern sogar mit, wenn die Datei kopiert wird.
Als Rückfallebene dient eine lokale Zuordnungsdatei über den Datei-URL —
für Formate, die keine solchen Eigenschaften kennen.
### Betriebssystemneutral
Die Erweiterung läuft unter Linux, Windows und macOS. Was dafür nötig war:
| Punkt | Umsetzung |
|---|---|
| Konfigurationsort | `%APPDATA%` unter Windows, sonst `XDG_CONFIG_HOME` bzw. `~/.config` |
| Pfade | durchgehend `os.path.join`, nie feste Trennzeichen |
| Datei-URLs | `unohelper.systemPathToFileUrl` statt eigener Zusammensetzung |
| Dateinamen | Windows-Verbotszeichen und Gerätenamen werden auch unter Linux entfernt |
Eine Einschränkung: `chmod` ist unter Windows weitgehend wirkungslos. Die
Einstellungsdatei mit dem Token liegt dort im Benutzerprofil, auf das
andere Benutzer standardmäßig keinen Zugriff haben — der Schutz kommt also
vom Betriebssystem statt von den Dateirechten.
`build.sh` ist ein Shell-Skript und läuft nur unter Linux und macOS. Es
wird ausschließlich zur Entwicklung gebraucht; die fertige `.oxt` lässt
sich überall installieren.
### Ohne `requests`
Die in LibreOffice mitgelieferte Python-Laufzeit bringt nur die
Standardbibliothek mit. Alles läuft über `urllib`, inklusive
selbstgebautem Multipart-Upload.
---
## Bauen und installieren
```bash
./build.sh
unopkg add --force paperless-libreoffice-1.1.0.oxt
```
> **Zur Versionsabhängigkeit:** `OpenOffice.org-minimal-version` in
> `description.xml` bezieht sich auf die OpenOffice-**Referenzversion**, und
> die ist bei LibreOffice dauerhaft auf **4.1** eingefroren — unabhängig
> davon, ob die installierte Version 7, 24 oder 25 heißt. Ein Wert wie `7.0`
> schließt dort jede Installation aus und führt zu „Die Erweiterung benötigt
> mindestens OpenOffice.org Referenzversion 7.0".
>
> Für echte LibreOffice-Versionen gibt es `lo:LibreOffice-minimal-version`
> im Namensraum `http://libreoffice.org/extensions/description/2011`.
LibreOffice danach **vollständig beenden**, auch den Schnellstarter:
```bash
pkill soffice
```
Entfernen:
```bash
unopkg remove org.example.paperless.libreoffice
```
Alternativ über Extras → Extension Manager.
---
## Einrichten
1. **Extras → Paperless → Verbindung einrichten**
2. Basisadresse eintragen, etwa `https://dms.example.org` — ohne
abschließenden Schrägstrich
3. API-Token eintragen. Erzeugt wird er im **Benutzerprofil** von
Paperless, nicht in der allgemeinen Verwaltung
4. Speichern — die Verbindung wird sofort geprüft
Die Einstellungen liegen unter
`~/.config/paperless-libreoffice/settings.json` mit Modus 600.
> **Zum Token:** Er liegt im Klartext in dieser Datei. Das entspricht dem,
> was andere Desktop-Anwendungen tun, ist aber kein Geheimnisspeicher. Wer
> es strenger will, müsste die Erweiterung an den Systemschlüsselbund
> anbinden — über `com.sun.star.task.PasswordContainer` oder libsecret.
---
## Ablauf im Alltag
```
Extras → Paperless → Aus Paperless öffnen…
Suchbegriff oder Archiv-Seriennummer eingeben
Dokument auswählen → öffnet sich in LibreOffice
… bearbeiten, zwischendurch normal mit Strg+S speichern
(bleibt lokal, das Archiv bleibt unberührt)
Extras → Paperless → Als neue Version ablegen
Rückfrage bestätigen → Upload → Bestätigung mit Versions-ID
```
Die bisherige Fassung bleibt als Version erhalten. In Paperless entsteht
kein neuer Eintrag, sondern eine Version am bestehenden Dokument.
---
## Fehlersuche
Die Erweiterung erscheint nicht im Menü:
```bash
unopkg list | grep -i paperless
```
Fehlt sie, wurde LibreOffice nicht vollständig beendet. Python-Fehler beim
Laden erscheinen auf der Konsole:
```bash
soffice --writer
```
Ausführlichere Meldungen mit:
```bash
SAL_LOG="+WARN+INFO.extensions" soffice --writer
```
### Bekannte Stolpersteine
**Dokument ohne Speicherort.** Ein noch nie gespeichertes Dokument wird
für die Ablage automatisch temporär geschrieben. In LibreOffice bleibt es
danach weiterhin ungespeichert — das ist Absicht, damit nicht ungefragt
eine Datei im Dateisystem entsteht, von der niemand weiß.
**„Dieses Dokument stammt nicht aus Paperless."** Die Kennung wurde nicht
gefunden. Das passiert, wenn die Datei über den normalen Öffnen-Dialog
geladen wurde oder das Format keine benutzerdefinierten
Dokumenteigenschaften kennt.
**Speichern in Fremdformaten.** Beim Zurückspeichern eines DOCX oder XLSX
wird der passende Filter gewählt (siehe `FORMATS` in
`pythonpath/paperless_formats.py`). Fehlt ein Format dort, speichert
LibreOffice als ODF — dann stimmt die Endung nicht mehr zum Inhalt.
**Freigabelinks.** Der Endpunkt heißt je nach Paperless-Version
`share_links/` oder `sharelinks/`. Beide werden probiert.
**HTTP 403 nur über die Domain, nicht über die IP.** Klassisches Zeichen
dafür, dass ein Schutzsystem vor der Anwendung den User-Agent abweist.
`urllib` sendet ohne Zutun `Python-urllib/3.x`, und diese Kennung steht in
den Sperrlisten von CrowdSec, fail2ban und gängigen WAF-Regelwerken.
Die Erweiterung setzt deshalb eine eigene Kennung. Wird auch die
abgewiesen, lässt sie sich in `paperless_client.py` über die Konstante
`USER_AGENT` ändern.
Zum Nachstellen:
```bash
curl -s -o /dev/null -w '%{http_code}\n' -A "Python-urllib/3.10" \
-H "Authorization: Token $PT" https://dms.example.org/api/documents/
```
403 gegenüber 200 mit der curl-Vorgabe bestätigt die Ursache. Die
Fehlermeldung kann dabei von einem völlig anderen Dienst stammen als dem,
den man ansprechen wollte.
---
## Symbole
Alle Symbole teilen dieselbe Grundform — ein Blatt mit umgeknickter Ecke in
Paperless-Grün — damit die Leiste als zusammengehörige Gruppe erkennbar
bleibt. Unterschieden wird über eine farbige Scheibe unten rechts:
| Befehl | Beizeichen |
|---|---|
| **Markensymbol** | gefüllte grüne Fläche mit weißem Blatt, ganz links in der Leiste |
| Öffnen | blauer Pfeil nach unten |
| Neue Version | oranger Pfeil nach oben mit Plus |
| In Paperless ablegen | oranger Pfeil nach oben mit Sternchen, zweites Blatt dahinter |
| Klassifizierung | violettes Etikett |
| Teilen | türkise verbundene Punkte |
| Einstellungen | graues Zahnrad |
Das Markensymbol steht am Anfang der Leiste, durch einen Trenner von den
Befehlen abgesetzt. Es ist mit **Status anzeigen** belegt, also nicht nur
Zierde. Durch die gefüllte Fläche unterscheidet es sich deutlich von den
Bordmitteln von LibreOffice — man sieht auf einen Blick, wo die
Paperless-Leiste beginnt und endet.
### Format
Registriert sind **SVG**. LibreOffice skaliert sie auf die eingestellte
Symbolgröße (Extras → Optionen → Ansicht → Symbolgröße: Klein, Groß, Sehr
groß). Mit PNG wären nur zwei feste Größen möglich, und bei „Sehr groß"
würde hochgerechnet.
Erzeugt werden die Symbole mit `make_icons.py` aus einer **gemeinsamen
Geometriebeschreibung** — SVG und PNG können deshalb nicht auseinanderlaufen.
Das Skript braucht Pillow für die PNG-Ausgabe und wird nur zur Entwicklung
gebraucht; die fertigen Dateien liegen unter `icons/` und sind im Paket.
Zusätzlich zu den SVG enthält das Paket PNG in 16, 26 und 32 Pixeln als
Rückfallebene. Sollte die SVG-Unterstützung in einer LibreOffice-Version
nicht greifen und gar kein Symbol erscheinen, in `Addons.xcu` umstellen:
```
ImageSmallURL → %origin%/icons/paperless-NAME_16.png
ImageBigURL → %origin%/icons/paperless-NAME_26.png
```
### Reihenfolge in der Leiste
```
[Paperless] │ [Öffnen] [Neue Version] [Ablegen unter] [Klassifizierung] [Teilen] │ [Verbindung]
```
## Aufbau
```
paperless_addon.py Protokoll-Handler, Befehlslogik
make_icons.py erzeugt die Symbole (nur zur Entwicklung)
pythonpath/
paperless_client.py REST-Client, nur Standardbibliothek
paperless_config.py Einstellungen, Dokumentzuordnung
paperless_dialogs.py UNO-Dialoge, programmatisch aufgebaut
paperless_formats.py Formate, Filter, Bearbeitbarkeit
Addons.xcu Menü, Symbolleiste, Symbole
icons/ 16x16 und 26x26 PNG je Befehl
ProtocolHandler.xcu Registrierung von paperless.Command:*
```
Der Protokoll-Handler ist der Kern: Menüeinträge lösen URLs wie
`paperless.Command:Open` aus, die LibreOffice an die registrierte Klasse
weiterreicht.
---
## Stand und Grenzen
Version 1.1.0. Das Gerüst ist vollständig und die Client-Logik getestet,
aber die Erweiterung ist an einer echten Instanz noch nicht über längere
Zeit erprobt.
Nicht enthalten:
- **Zusatzfelder** im Klassifizierungsdialog — nur die Grundfelder
- **Fortschrittsanzeige** beim Upload — bei großen Dateien wirkt
LibreOffice währenddessen eingefroren
- **Übersetzungen** über die Oberflächentexte hinaus
- **Konfliktbehandlung**, wenn dasselbe Dokument parallel anderswo
geändert wurde
Der letzte Punkt ist der relevanteste für den Mehrbenutzerbetrieb.
Paperless kennt keine Sperren; wer zeitgleich arbeitet, erzeugt zwei
Versionen statt eines Konflikts. Bei einem kleinen Kreis ist das
verschmerzbar.
Die Software wird ohne jegliche Gewähr oder Haftungsübernahme bereitgestellt.
Die Nutzung erfolgt auf eigene Gefahr.