saltcorn-samba 0.4.14 → 0.4.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,57 @@ All notable changes to `saltcorn-samba` are documented here.
4
4
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
5
5
  and this project adheres to [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## [0.4.15] – 2026-07-07
8
+
9
+ ### Fixed – CSRF-Fehlermeldung bei Upload / Ordner anlegen / Rename / Delete
10
+
11
+ Bei jedem schreibenden Klick in der File-Manager-View („Ordner anlegen“,
12
+ „Datei hochladen“, „umbenennen“, „löschen“) erschien der Fehler
13
+ `Invalid CSRF token`, obwohl der Client den Token korrekt im Header
14
+ `X-CSRF-Token` mitgeschickt hat.
15
+
16
+ **Root Cause:** Saltcorns globaler `csurf`-Middleware validiert bereits
17
+ alle POST-Routen der Plugins gegen das Session-Secret. Unser Plugin hatte
18
+ zusätzlich eine manuelle `checkCsrf(req)`-Funktion, die den vom Client
19
+ geschickten Token mit `req.csrfToken()` verglich. `req.csrfToken()` gibt
20
+ aber bei jedem Aufruf einen **frisch gesalzenen** Token zurück – der
21
+ Stringvergleich `provided !== req.csrfToken()` schlug daher immer fehl,
22
+ auch bei völlig gültigen Requests.
23
+
24
+ **Fix:** `checkCsrf` ist jetzt ein dokumentierter No-Op. Die eigentliche
25
+ CSRF-Prüfung übernimmt weiterhin Saltcorns Middleware (kein Sicherheits-
26
+ abbau). Betroffene Write-Routen (Upload, Mkdir, Rename, Delete) in
27
+ `index.js` sind unverändert und funktionieren jetzt korrekt.
28
+
29
+ ### Added – View-Basispfad (`view_base_path`) für File-Manager und Tree
30
+
31
+ Beide Views (SambaFileManager und SambaTree) haben ein neues optionales
32
+ Konfigurationsfeld **„View-Basispfad (relativ zum Plugin-Basispfad)“**.
33
+ Damit lässt sich der Pfad zweistufig konfigurieren:
34
+
35
+ 1. **Plugin-Basispfad** (weiterhin Pflicht) – z. B. `static` – legt fest,
36
+ in welchem Unterverzeichnis der Freigabe der Plugin-Root liegt.
37
+ 2. **View-Basispfad** (neu, optional) – z. B. `projekte/2026` – ein
38
+ statischer Präfix, der nur für diese eine View gilt.
39
+
40
+ Beispiel: Plugin-Basispfad = `static`, View-Basispfad = `projekte/2026`
41
+ → die View listet den effektiven Pfad `static/projekte/2026`.
42
+
43
+ Der bisherige `extra_subpath` in beiden Views ist jetzt konsistent auf
44
+ den `from_field`-Modus beschränkt (dort weiterhin: Suffix hinter dem
45
+ Feldwert der aktuellen Zeile). Im `static`-Modus zählt nur
46
+ `view_base_path`.
47
+
48
+ Der zusammengesetzte Pfad wird pro Segment slash-getrimmt und
49
+ durchläuft `sanitizeRelativePath`, so dass `..`-Traversal, absolute
50
+ Pfade, UNC-Präfixe usw. weiterhin abgelehnt werden.
51
+
52
+ ### Tests
53
+
54
+ `test/sanitize.test.js` deckt jetzt zusätzlich die Pfad-Komposition
55
+ (`view_base_path` + Feldwert + `extra_subpath`) inklusive Slash-
56
+ Normalisierung, Modus-Regeln und Traversal-Ablehnung ab.
57
+
7
58
  ## [0.4.14] – 2026-07-06
8
59
 
9
60
  ### Changed – Code-Review / Aufräumen (keine Verhaltensänderung)
package/README.md CHANGED
@@ -194,9 +194,10 @@ Optionen:
194
194
 
195
195
  | Option | Zweck |
196
196
  |---|---|
197
- | **Root directory mode** | `static` = immer Base-Path aus Plugin-Config, `from_field` = Sub-Pfad kommt aus einem DB-Feld der aktuellen Zeile |
198
- | **Row field with sub-path** | Feld der Tabelle mit dem relativen Ordnernamen (bei `from_field`) |
199
- | **Extra sub-path** | statischer Suffix (z. B. `invoices`) |
197
+ | **View-Basispfad (relativ zum Plugin-Basispfad)** | Optionaler statischer Präfix nur für diese View. Beispiel: Plugin-Basispfad = `static`, View-Basispfad = `projekte/2026` View listet `static/projekte/2026`. Zweistufige Konfiguration: einmal serverweit im Plugin, einmal pro View. |
198
+ | **Row-Modus** | `static` = nur der View-Basispfad zählt, `from_field` = zusätzlich wird ein DB-Feldwert der aktuellen Zeile angehängt (z. B. in einer Show-View) |
199
+ | **Feld mit Unterpfad** | Feld der Tabelle mit dem relativen Ordnernamen (nur bei `from_field`) |
200
+ | **Zusätzlicher Suffix (nach dem Feldwert)** | statischer Suffix, z. B. `invoices` (nur bei `from_field`) |
200
201
  | **Show hidden files** | `.dotfiles` einblenden |
201
202
  | **Allow navigating up** | Up-Button aktivieren (nur bis zum Root, kein Ausbruch) |
202
203
  | **Open PDFs / images inline** | Klick öffnet die Datei im integrierten Viewer |
@@ -204,6 +205,10 @@ Optionen:
204
205
  | **Page size** | Einträge pro Seite (0 = alle) |
205
206
  | **Panel title** | Text im Karten-Header |
206
207
 
208
+ Der effektive Pfad wird zusammengesetzt aus (jeweils falls gesetzt):
209
+ `Plugin-Basispfad / View-Basispfad / Feldwert / Zusätzlicher Suffix`.
210
+ `..`-Traversal, absolute Pfade und UNC-Präfixe werden abgelehnt.
211
+
207
212
  Beispiel-Datenmodell für kunden-spezifische Ordner:
208
213
 
209
214
  ```
@@ -212,12 +217,13 @@ Felder: name (String), akte_dir (String, z. B. "kunde_42/2026")
212
217
  ```
213
218
 
214
219
  Dann eine Show-View von `kunden` bauen und die `SambaFileManager`-View mit
215
- Mode = `from_field`, Row field = `akte_dir` einbetten.
220
+ Row-Modus = `from_field`, Feld = `akte_dir` einbetten.
216
221
 
217
222
  ### View: `SambaTree`
218
223
 
219
224
  Kompaktere Alternative – lazy-loading Baum, ideal in einer Sidebar oder
220
- neben Formularen. Gleiche Path-Modi wie beim File-Manager.
225
+ neben Formularen. Gleiche Path-Optionen wie beim File-Manager (inkl.
226
+ **View-Basispfad**, **Row-Modus**, **Feld**, **Zusätzlicher Suffix**).
221
227
 
222
228
  ### Fieldview `samba_pdf` (deaktiviert seit 0.3.2)
223
229
 
@@ -44,31 +44,42 @@ const configuration_workflow = () =>
44
44
  .map((f) => f.name);
45
45
  return new Form({
46
46
  fields: [
47
+ new Field({
48
+ name: "view_base_path",
49
+ label: "View-Basispfad (relativ zum Plugin-Basispfad)",
50
+ type: "String",
51
+ sublabel:
52
+ "Optional. Statischer Pfad relativ zum Plugin-Basispfad, der f\u00fcr diese View gilt. " +
53
+ "Beispiel: Plugin-Basispfad = 'static', View-Basispfad = 'projekte/2026' \u2192 View listet 'static/projekte/2026'. " +
54
+ "Keine f\u00fchrenden/abschlie\u00dfenden Slashes n\u00f6tig. Traversal (\u201e..\u201c) wird abgelehnt.",
55
+ }),
47
56
  new Field({
48
57
  name: "mode",
49
- label: "Root directory mode",
58
+ label: "Row-Modus",
50
59
  type: "String",
51
60
  required: true,
52
61
  attributes: {
53
62
  options: ["static", "from_field"],
54
63
  },
55
64
  sublabel:
56
- "static = always the plugin base_path. from_field = append a row field value to base_path (only when the view is embedded in a Show view).",
65
+ "static = immer nur der View-Basispfad. from_field = h\u00e4nge einen Feldwert der aktuellen Zeile an " +
66
+ "(nur sinnvoll, wenn die View in einer Show-View eingebettet ist).",
57
67
  default: "static",
58
68
  }),
59
69
  new Field({
60
70
  name: "path_field",
61
- label: "Row field with sub-path",
71
+ label: "Feld mit Unterpfad",
62
72
  type: "String",
63
73
  attributes: { options: stringFieldOptions },
64
74
  showIf: { mode: "from_field" },
65
75
  }),
66
76
  new Field({
67
77
  name: "extra_subpath",
68
- label: "Extra sub-path (appended)",
78
+ label: "Zus\u00e4tzlicher Suffix (nach dem Feldwert)",
69
79
  type: "String",
70
80
  sublabel:
71
- "Optional. Static suffix appended after the field value. Example: 'invoices'",
81
+ "Optional. Statischer Suffix, der nach dem Feldwert angeh\u00e4ngt wird. Beispiel: 'invoices'.",
82
+ showIf: { mode: "from_field" },
72
83
  }),
73
84
  new Field({
74
85
  name: "show_hidden",
@@ -122,14 +133,37 @@ async function get_state_fields() {
122
133
  return [];
123
134
  }
124
135
 
136
+ /**
137
+ * Compose the start path (relative to the plugin base_path) from three
138
+ * optional parts:
139
+ * 1. `view_base_path` — static, per-view prefix
140
+ * 2. row field value — only in "from_field" mode
141
+ * 3. `extra_subpath` — static suffix, only in "from_field" mode
142
+ *
143
+ * Empty parts are dropped. The final joined path is passed through
144
+ * sanitizeRelativePath so traversal / absolute paths / backslashes are
145
+ * rejected consistently.
146
+ */
125
147
  function computeStartPath(configuration, row) {
126
148
  const parts = [];
149
+ if (configuration.view_base_path) {
150
+ parts.push(String(configuration.view_base_path));
151
+ }
127
152
  if (configuration.mode === "from_field" && configuration.path_field && row) {
128
153
  const v = row[configuration.path_field];
129
154
  if (v) parts.push(String(v));
130
155
  }
131
- if (configuration.extra_subpath) parts.push(String(configuration.extra_subpath));
132
- return sanitizeRelativePath(parts.join("/"));
156
+ if (
157
+ configuration.mode === "from_field" &&
158
+ configuration.extra_subpath
159
+ ) {
160
+ parts.push(String(configuration.extra_subpath));
161
+ }
162
+ const joined = parts
163
+ .map((p) => p.replace(/^\/+|\/+$/g, ""))
164
+ .filter(Boolean)
165
+ .join("/");
166
+ return sanitizeRelativePath(joined);
133
167
  }
134
168
 
135
169
  // ---------------------------------------------------------------------------
package/index.js CHANGED
@@ -519,22 +519,18 @@ function jsonOk(res, extra) {
519
519
  }
520
520
 
521
521
  /**
522
- * Validate that the request carries a CSRF token matching the session.
523
- * Saltcorn injects `req.csrfToken()` when the CSRF middleware is active;
524
- * we accept either the `_csrf` body field or the `x-csrf-token` header.
522
+ * CSRF verification is handled by Saltcorn's global csurf middleware
523
+ * (packages/server/app.js). By the time our route handler runs, csurf
524
+ * has already validated req.body._csrf / X-CSRF-Token / csrf-token /
525
+ * xsrf-token headers against the session secret. A manual re-check
526
+ * here would be wrong: csrf-tokens returns a NEW salted token on
527
+ * every call to req.csrfToken(), so `provided !== req.csrfToken()`
528
+ * would fail even for perfectly valid requests.
529
+ *
530
+ * We keep this stub as the single documented spot to attach future
531
+ * pre-flight authorisation checks if ever needed.
525
532
  */
526
- function checkCsrf(req, res) {
527
- if (typeof req.csrfToken !== "function") return true; // CSRF disabled globally
528
- const expected = req.csrfToken();
529
- const provided =
530
- (req.body && req.body._csrf) ||
531
- req.headers["x-csrf-token"] ||
532
- req.headers["csrf-token"] ||
533
- req.query._csrf;
534
- if (!provided || provided !== expected) {
535
- jsonError(res, 403, "Invalid CSRF token");
536
- return false;
537
- }
533
+ function checkCsrf(_req, _res) {
538
534
  return true;
539
535
  }
540
536
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "saltcorn-samba",
3
- "version": "0.4.14",
3
+ "version": "0.4.15",
4
4
  "description": "Saltcorn plugin: browse, upload, rename and delete files on a Samba/CIFS share via SMB 3.1.1 (AES-CMAC signing, optional encryption). File-manager view, directory tree, inline PDF viewer, external-app open (smb://).",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/tree-view.js CHANGED
@@ -41,30 +41,42 @@ const configuration_workflow = () =>
41
41
  .map((f) => f.name);
42
42
  return new Form({
43
43
  fields: [
44
+ new Field({
45
+ name: "view_base_path",
46
+ label: "View-Basispfad (relativ zum Plugin-Basispfad)",
47
+ type: "String",
48
+ sublabel:
49
+ "Optional. Statischer Pfad relativ zum Plugin-Basispfad, der f\u00fcr diese View gilt. " +
50
+ "Beispiel: Plugin-Basispfad = 'static', View-Basispfad = 'projekte/2026' \u2192 View listet 'static/projekte/2026'. " +
51
+ "Keine f\u00fchrenden/abschlie\u00dfenden Slashes n\u00f6tig. Traversal (\u201e..\u201c) wird abgelehnt.",
52
+ }),
44
53
  new Field({
45
54
  name: "mode",
46
- label: "Path mode",
55
+ label: "Row-Modus",
47
56
  type: "String",
48
57
  required: true,
49
58
  attributes: {
50
59
  options: ["static", "from_field"],
51
60
  },
52
61
  sublabel:
53
- "static = always show the plugin base_path; from_field = append a row field value",
62
+ "static = immer nur der View-Basispfad. from_field = h\u00e4nge einen Feldwert der aktuellen Zeile an " +
63
+ "(nur sinnvoll, wenn die View in einer Show-View eingebettet ist).",
64
+ default: "static",
54
65
  }),
55
66
  new Field({
56
67
  name: "path_field",
57
- label: "Row field with sub-path",
68
+ label: "Feld mit Unterpfad",
58
69
  type: "String",
59
70
  attributes: { options: stringFieldOptions },
60
71
  showIf: { mode: "from_field" },
61
72
  }),
62
73
  new Field({
63
74
  name: "extra_subpath",
64
- label: "Extra sub-path (appended)",
75
+ label: "Zus\u00e4tzlicher Suffix (nach dem Feldwert)",
65
76
  type: "String",
66
77
  sublabel:
67
- "Optional. Static suffix appended after the field value. Example: 'invoices'",
78
+ "Optional. Statischer Suffix, der nach dem Feldwert angeh\u00e4ngt wird. Beispiel: 'invoices'.",
79
+ showIf: { mode: "from_field" },
68
80
  }),
69
81
  new Field({
70
82
  name: "show_hidden",
@@ -104,17 +116,35 @@ async function get_state_fields(table_id, viewname, { mode, path_field }) {
104
116
  }
105
117
 
106
118
  /**
107
- * Given the view configuration + a Saltcorn row, compute the effective
108
- * relative path (below base_path) that the tree should start from.
119
+ * Compose the start path (relative to the plugin base_path) from three
120
+ * optional parts:
121
+ * 1. `view_base_path` — static, per-view prefix
122
+ * 2. row field value — only in "from_field" mode
123
+ * 3. `extra_subpath` — static suffix, only in "from_field" mode
124
+ *
125
+ * Empty parts are dropped and slash-trimmed. The final joined path is
126
+ * passed through sanitizeRelativePath so traversal / absolute paths /
127
+ * backslashes are rejected consistently.
109
128
  */
110
129
  function computeStartPath(configuration, row) {
111
130
  const parts = [];
131
+ if (configuration.view_base_path) {
132
+ parts.push(String(configuration.view_base_path));
133
+ }
112
134
  if (configuration.mode === "from_field" && configuration.path_field && row) {
113
135
  const v = row[configuration.path_field];
114
136
  if (v) parts.push(String(v));
115
137
  }
116
- if (configuration.extra_subpath) parts.push(String(configuration.extra_subpath));
117
- const joined = parts.join("/");
138
+ if (
139
+ configuration.mode === "from_field" &&
140
+ configuration.extra_subpath
141
+ ) {
142
+ parts.push(String(configuration.extra_subpath));
143
+ }
144
+ const joined = parts
145
+ .map((p) => p.replace(/^\/+|\/+$/g, ""))
146
+ .filter(Boolean)
147
+ .join("/");
118
148
  // sanitizeRelativePath throws on traversal – we let the caller handle it.
119
149
  return sanitizeRelativePath(joined);
120
150
  }