fix(quick-261009-dkv): Pfade woertlich, Zaehler und Serverdatum, Rechte des Ordners (Schnittstelle)
- Pfade, Ziele und Empfaengerkennungen der Freigaben bleiben woertlich (nur Anzeigetexte werden bereinigt) - Weitergaben als eigene Freigaben mit eigenem Pfad; accessOf nach Eintragsart - Begrenzung: 10 neue Freigaben je 10 Minuten, 40 Versuche je 10 Minuten vor den Abfragen an die Nextcloud, leere Zaehler werden entfernt - Freigaberegeln nennen das Serverdatum, die Ordnerliste die Berechtigungsbuchstaben des Ordners selbst - Live-Test: Ablehnen offener Freigaben, Weitergabe, Buchstaben des Ordners Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -37,7 +37,10 @@ export type NcItemType = 'file' | 'folder';
|
||||
export interface NcShareView {
|
||||
id: string;
|
||||
kind: NcShareKind;
|
||||
/** Pfad im Bereich des Eigentuemers (bei eigenen Freigaben im eigenen Konto). */
|
||||
/**
|
||||
* Pfad im Baum des Aufrufers, woertlich wie von der Nextcloud geliefert (nie bereinigt: er geht
|
||||
* zurueck an die Nextcloud, ein zusammengefasstes Leerzeichen waere ein anderer Eintrag).
|
||||
*/
|
||||
path: string;
|
||||
name: string;
|
||||
itemType: NcItemType;
|
||||
@@ -49,8 +52,14 @@ export interface NcShareView {
|
||||
/** Kennung von Person/Gruppe; bei Links null. */
|
||||
shareWith: string | null;
|
||||
shareWithName: string | null;
|
||||
/**
|
||||
* Wer die Freigabe erstellt hat (`uid_owner` der Nextcloud heisst dort "Freigebender", nicht
|
||||
* "Eigentuemer der Datei"; gemessen). Bei eigenen Freigaben ist das der Benutzer selbst.
|
||||
*/
|
||||
ownerId: string | null;
|
||||
ownerName: string | null;
|
||||
/** Anzeigename des Dateieigentuemers, nur wenn er ein anderer ist (Weitergabe, siehe `parseShare`). */
|
||||
fileOwnerName: string | null;
|
||||
canEdit: boolean;
|
||||
canDelete: boolean;
|
||||
/** `YYYY-MM-DD` oder null. */
|
||||
@@ -59,7 +68,7 @@ export interface NcShareView {
|
||||
/** Nur bei eigenen Links, nur http/https. */
|
||||
url: string | null;
|
||||
hasPassword: boolean;
|
||||
/** Pfad im eigenen Baum des Empfaengers (bei eigenen Freigaben der eigene Pfad). */
|
||||
/** Pfad im eigenen Baum des Aufrufers (bei eigenen Freigaben und Weitergaben gleich `path`), woertlich. */
|
||||
target: string;
|
||||
sharedAt: string | null;
|
||||
/** Noch nicht angenommen (nur eingehende). */
|
||||
@@ -74,6 +83,8 @@ export interface NcSharee {
|
||||
}
|
||||
|
||||
export interface NcSharePolicy {
|
||||
/** Heutiges Datum (`YYYY-MM-DD`) nach der Uhr des Tessera-Servers: Grundlage fuer Ablaufgrenzen. */
|
||||
today: string;
|
||||
/** Freigabe-Schnittstelle der Nextcloud an. */
|
||||
enabled: boolean;
|
||||
groupsEnabled: boolean;
|
||||
@@ -108,6 +119,7 @@ const SHARE_TIMEOUT_MS = 15_000;
|
||||
const MESSAGE_MAX = 300;
|
||||
const DISPLAY_MAX = 255;
|
||||
const URL_MAX = 2048;
|
||||
const PATH_MAX = 4096;
|
||||
|
||||
const SHARE_ID_RE = /^\d{1,20}$/;
|
||||
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
||||
@@ -133,6 +145,18 @@ export function cleanText(value: unknown, max: number): string {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pfade und Kennungen (Freigabe-Pfad, Ziel, Empfaenger) gehen unveraendert an die Nextcloud
|
||||
* zurueck. Sie werden darum NIE zusammengefasst oder gekuerzt wie Anzeigetexte: ein doppeltes
|
||||
* oder nachgestelltes Leerzeichen gehoert zum Namen. Enthalten sie Steuerzeichen oder sind sie
|
||||
* zu lang, gelten sie als unbrauchbar (leere Zeichenkette).
|
||||
*/
|
||||
export function verbatimId(value: unknown, max: number): string {
|
||||
if (typeof value !== 'string' || value.length > max) return '';
|
||||
// biome-ignore lint/suspicious/noControlCharactersInRegex: Steuerzeichen sind hier gerade der Pruefstoff
|
||||
return /[\u0000-\u001f\u007f]/.test(value) ? '' : value;
|
||||
}
|
||||
|
||||
function intOf(value: unknown): number | null {
|
||||
if (typeof value === 'number' && Number.isFinite(value)) return Math.trunc(value);
|
||||
if (typeof value === 'string' && /^-?\d{1,15}$/.test(value.trim())) return Number(value.trim());
|
||||
@@ -238,12 +262,20 @@ export function permissionsFor(
|
||||
return itemType === 'folder' ? CREATE : null;
|
||||
}
|
||||
|
||||
/** Umkehrung: aus der Maske (Bit 16 wird ignoriert) die einfache Auswahl oder `custom`. */
|
||||
export function accessOf(permissions: number): NcShareAccess {
|
||||
/**
|
||||
* Umkehrung: aus der Maske (Bit 16 wird ignoriert) die einfache Auswahl oder `custom`. Nur die
|
||||
* Masken, die `permissionsFor` selbst vergibt, gelten als Auswahl: Ansehen 1; Bearbeiten 15
|
||||
* (Ordner) bzw. 3 (Datei); Nur hochladen 4 (nur Ordner). Alles andere (z. B. Lesen plus Loeschen
|
||||
* oder Lesen plus Anlegen) ist eine eigene Berechtigung und wird so angezeigt, nie als
|
||||
* Bearbeiten ausgegeben.
|
||||
*/
|
||||
export function accessOf(permissions: number, itemType: NcItemType): NcShareAccess {
|
||||
const mask = permissions & 15;
|
||||
if (mask === READ) return 'view';
|
||||
if (mask === CREATE) return 'upload';
|
||||
if ((mask & READ) !== 0 && (mask & (UPDATE | CREATE | DELETE)) !== 0) return 'edit';
|
||||
if (itemType === 'folder' && mask === CREATE) return 'upload';
|
||||
if (mask === (itemType === 'folder' ? READ | UPDATE | CREATE | DELETE : READ | UPDATE)) {
|
||||
return 'edit';
|
||||
}
|
||||
return 'custom';
|
||||
}
|
||||
|
||||
@@ -260,6 +292,12 @@ export function isRealDate(value: string): boolean {
|
||||
return !Number.isNaN(d.getTime()) && d.toISOString().slice(0, 10) === value;
|
||||
}
|
||||
|
||||
/** Datum `YYYY-MM-DD` nach der Uhr dieses Servers (nicht des Browsers): Grundlage der Ablaufgrenzen. */
|
||||
export function serverDate(now: Date = new Date()): string {
|
||||
const pad = (n: number, width = 2) => String(n).padStart(width, '0');
|
||||
return `${pad(now.getFullYear(), 4)}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;
|
||||
}
|
||||
|
||||
function publicUrl(value: unknown): string | null {
|
||||
if (typeof value !== 'string' || value.length === 0 || value.length > URL_MAX) return null;
|
||||
try {
|
||||
@@ -270,7 +308,15 @@ function publicUrl(value: unknown): string | null {
|
||||
}
|
||||
}
|
||||
|
||||
/** `null` fuer nicht unterstuetzte Arten (E-Mail, Server, Talk ...) und fehlerhafte Eintraege. */
|
||||
/**
|
||||
* `null` fuer nicht unterstuetzte Arten (E-Mail, Server, Talk ...) und fehlerhafte Eintraege.
|
||||
*
|
||||
* Eingehend/eigen (gemessen, Nextcloud 34): `uid_owner` ist der FREIGEBENDE, `uid_file_owner`
|
||||
* der Eigentuemer der Datei. Eine Weitergabe (Ben gibt einen Ordner weiter, den Anna ihm
|
||||
* geteilt hat) hat `uid_owner = ben`, `uid_file_owner = anna`; `path` ist immer der Pfad im
|
||||
* Baum des Aufrufers, `file_target` der im Baum des Empfaengers. Weitergaben gelten daher als
|
||||
* eigene Freigaben mit eigenem Pfad (`target` = `path`), nicht als eingehende.
|
||||
*/
|
||||
export function parseShare(raw: unknown, selfId: string): NcShareView | null {
|
||||
if (!isDict(raw)) return null;
|
||||
const type = intOf(raw.share_type);
|
||||
@@ -282,11 +328,15 @@ export function parseShare(raw: unknown, selfId: string): NcShareView | null {
|
||||
|
||||
const ownerId = cleanText(raw.uid_owner, DISPLAY_MAX) || null;
|
||||
const received = ownerId !== null && ownerId !== selfId;
|
||||
const target = cleanText(raw.file_target, 4096);
|
||||
const path = cleanText(raw.path, 4096) || target;
|
||||
const rawTarget = verbatimId(raw.file_target, PATH_MAX);
|
||||
const rawPath = verbatimId(raw.path, PATH_MAX);
|
||||
// Ohne brauchbaren Pfad laesst sich nichts damit tun: wie eine unbekannte Art nur gezaehlt.
|
||||
if (rawPath === '' && rawTarget === '') return null;
|
||||
const path = rawPath || rawTarget;
|
||||
const target = received ? rawTarget || rawPath : path;
|
||||
const itemType: NcItemType =
|
||||
raw.item_type === 'folder' || raw.mimetype === 'httpd/unix-directory' ? 'folder' : 'file';
|
||||
const name = lastSegment(received ? target || path : path || target);
|
||||
const name = lastSegment(received ? target : path);
|
||||
const permissions = intOf(raw.permissions) ?? 0;
|
||||
const itemPermissions = intOf(raw.item_permissions) ?? permissions;
|
||||
const expirationRaw = typeof raw.expiration === 'string' ? raw.expiration.slice(0, 10) : '';
|
||||
@@ -296,6 +346,9 @@ export function parseShare(raw: unknown, selfId: string): NcShareView | null {
|
||||
? new Date(stime * 1000).toISOString()
|
||||
: null;
|
||||
const mimeRaw = cleanText(raw.mimetype, DISPLAY_MAX);
|
||||
const fileOwnerId = cleanText(raw.uid_file_owner, DISPLAY_MAX);
|
||||
const reshare = !received && fileOwnerId !== '' && fileOwnerId !== selfId;
|
||||
const shareWith = kind === 'link' ? '' : verbatimId(raw.share_with, DISPLAY_MAX);
|
||||
|
||||
return {
|
||||
id,
|
||||
@@ -306,12 +359,15 @@ export function parseShare(raw: unknown, selfId: string): NcShareView | null {
|
||||
mime: itemType === 'file' && mimeRaw !== '' ? mimeRaw : null,
|
||||
itemWritable: (itemPermissions & (UPDATE | CREATE)) !== 0,
|
||||
permissions,
|
||||
access: accessOf(permissions),
|
||||
shareWith: kind === 'link' ? null : cleanText(raw.share_with, DISPLAY_MAX) || null,
|
||||
access: accessOf(permissions, itemType),
|
||||
shareWith: shareWith === '' ? null : shareWith,
|
||||
shareWithName:
|
||||
kind === 'link' ? null : cleanText(raw.share_with_displayname, DISPLAY_MAX) || null,
|
||||
ownerId,
|
||||
ownerName: cleanText(raw.displayname_owner, DISPLAY_MAX) || null,
|
||||
fileOwnerName: reshare
|
||||
? cleanText(raw.displayname_file_owner, DISPLAY_MAX) || fileOwnerId
|
||||
: null,
|
||||
canEdit: raw.can_edit === true,
|
||||
canDelete: raw.can_delete === true,
|
||||
expiration: isRealDate(expirationRaw) ? expirationRaw : null,
|
||||
@@ -356,7 +412,8 @@ function shareeList(value: unknown, wanted: 0 | 1): NcSharee[] {
|
||||
for (const item of value) {
|
||||
if (!isDict(item) || !isDict(item.value)) continue;
|
||||
if (intOf(item.value.shareType) !== wanted) continue;
|
||||
const id = cleanText(item.value.shareWith, DISPLAY_MAX);
|
||||
// Die Kennung geht unveraendert zurueck an die Nextcloud (Anlegen): nie bereinigen.
|
||||
const id = verbatimId(item.value.shareWith, DISPLAY_MAX);
|
||||
if (id === '') continue;
|
||||
const label = cleanText(item.label, DISPLAY_MAX) || id;
|
||||
const unique = cleanText(item.shareWithDisplayNameUnique, DISPLAY_MAX);
|
||||
@@ -398,11 +455,12 @@ function days(value: unknown): number | null {
|
||||
}
|
||||
|
||||
/**
|
||||
* Freigaberegeln aus `cloud/capabilities` (Antwort-`data` oder dessen `capabilities`).
|
||||
* Freigaberegeln aus `cloud/capabilities` (Antwort-`data` oder dessen `capabilities`); `today`
|
||||
* ist das Serverdatum (`serverDate`) und wird unveraendert in die Regeln uebernommen.
|
||||
* Fehlende Schluessel heissen "keine Regel", kein Fehler: ohne `expire_date.enabled` gibt
|
||||
* es keine Tage; sind Links aus, steht unter `public` nur `enabled: false`.
|
||||
*/
|
||||
export function parseSharePolicy(data: unknown): NcSharePolicy {
|
||||
export function parseSharePolicy(data: unknown, today: string): NcSharePolicy {
|
||||
const caps = isDict(data) && isDict(data.capabilities) ? data.capabilities : data;
|
||||
const files = isDict(caps) && isDict(caps.files_sharing) ? caps.files_sharing : null;
|
||||
const pwPolicy = isDict(caps) && isDict(caps.password_policy) ? caps.password_policy : null;
|
||||
@@ -410,6 +468,7 @@ export function parseSharePolicy(data: unknown): NcSharePolicy {
|
||||
const passwordMinLength = minLen !== null && minLen >= 1 && minLen <= 256 ? minLen : null;
|
||||
|
||||
const off: NcSharePolicy = {
|
||||
today,
|
||||
enabled: false,
|
||||
groupsEnabled: false,
|
||||
links: {
|
||||
@@ -444,6 +503,7 @@ export function parseSharePolicy(data: unknown): NcSharePolicy {
|
||||
const internal = expiry(pub.expire_date_internal);
|
||||
|
||||
return {
|
||||
today,
|
||||
enabled: true,
|
||||
groupsEnabled: files.group_sharing !== false,
|
||||
links: linksEnabled
|
||||
|
||||
Reference in New Issue
Block a user