diff --git a/LICENSE b/LICENSE index 68ce501..a7d966d 100644 --- a/LICENSE +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md index a2e3c84..01bba34 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,465 @@ -# libreoffice-paperless-connector +# Paperless-Connector für LibreOffice -Addon für Libreoffice um Paperless-NGX anzubinden. \ No newline at end of file +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.