implemented a comprehensive Nextcloud file sharing system with support for user, group, and public link shares with gating based on server-side permissions; added sharing management interfaces including a share sheet; updated the file list with visual badges for incoming shares and improved OCS API response handling.
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../../errors/server_exception.dart';
|
||||
import '../nextcloud_ocs.dart';
|
||||
import 'nextcloud_sharing_capabilities.dart';
|
||||
|
||||
/// Fetches the current user's Nextcloud capabilities via OCS
|
||||
/// `GET cloud/capabilities` and extracts the `files_sharing` block. This is the
|
||||
/// per-user, group-aware source of truth the sharing UI gates on — no custom
|
||||
/// backend involved.
|
||||
class GetNextcloudCapabilities {
|
||||
Future<NextcloudSharingCapabilities> run() async {
|
||||
final endpoint = NextcloudOcs.uri(
|
||||
'cloud/capabilities',
|
||||
queryParameters: {'format': 'json'},
|
||||
);
|
||||
final response = await http.get(endpoint, headers: NextcloudOcs.headers());
|
||||
if (response.statusCode != HttpStatus.ok) {
|
||||
throw ServerException(
|
||||
statusCode: response.statusCode,
|
||||
technicalDetails: 'cloud/capabilities: ${response.body}',
|
||||
);
|
||||
}
|
||||
final decoded = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
final data =
|
||||
(decoded['ocs'] as Map<String, dynamic>?)?['data']
|
||||
as Map<String, dynamic>?;
|
||||
final capabilities = data?['capabilities'] as Map<String, dynamic>?;
|
||||
final filesSharing = capabilities?['files_sharing'];
|
||||
if (filesSharing is! Map<String, dynamic>) {
|
||||
// Server doesn't advertise files_sharing (app disabled) — treat as no
|
||||
// sharing capability rather than failing the whole load.
|
||||
return const NextcloudSharingCapabilities();
|
||||
}
|
||||
final passwordPolicy = capabilities?['password_policy'];
|
||||
return NextcloudSharingCapabilities.fromFilesSharing(
|
||||
filesSharing,
|
||||
passwordPolicy: passwordPolicy is Map<String, dynamic>
|
||||
? passwordPolicy
|
||||
: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
/// Subset of Nextcloud's `files_sharing` capabilities block that the mobile
|
||||
/// sharing UI gates on. Nextcloud reports these per authenticated user, so a
|
||||
/// group that an admin excluded from creating public links sees
|
||||
/// `public.enabled == false` here — exactly how the web UI hides those buttons.
|
||||
///
|
||||
/// The block is deeply nested and varies between server versions, so this is
|
||||
/// parsed by hand from the raw OCS map with safe fallbacks rather than via
|
||||
/// code generation. Missing fields default to the most restrictive value so a
|
||||
/// newer/older server never accidentally unlocks a capability.
|
||||
class NextcloudSharingCapabilities {
|
||||
/// `files_sharing.api_enabled` — master switch. When false the user may not
|
||||
/// create any share (user, group or link).
|
||||
final bool apiEnabled;
|
||||
|
||||
/// `files_sharing.public.enabled` — public link shares allowed.
|
||||
final bool publicEnabled;
|
||||
|
||||
/// `files_sharing.public.multiple_links` — more than one link per file.
|
||||
final bool publicMultipleLinks;
|
||||
|
||||
/// `files_sharing.public.upload` — public upload / file-drop folders.
|
||||
final bool publicUploadEnabled;
|
||||
|
||||
/// `files_sharing.public.password.enforced` — a password is mandatory on
|
||||
/// public links, so the create flow must collect one upfront.
|
||||
final bool publicPasswordEnforced;
|
||||
|
||||
/// `files_sharing.public.expire_date.enabled`.
|
||||
final bool publicExpireEnabled;
|
||||
|
||||
/// `files_sharing.public.expire_date.days` — default/maximum lifetime.
|
||||
final int? publicExpireDays;
|
||||
|
||||
/// `files_sharing.public.expire_date.enforced` — expiry cannot be removed.
|
||||
final bool publicExpireEnforced;
|
||||
|
||||
/// `files_sharing.group.enabled` (falls back to the older `group_sharing`).
|
||||
final bool groupEnabled;
|
||||
|
||||
/// `files_sharing.resharing` — recipients may reshare.
|
||||
final bool resharing;
|
||||
|
||||
// --- password_policy (a sibling capability of files_sharing) ---
|
||||
// These let the link-password UI state the rules up front instead of only
|
||||
// surfacing them after the server rejects a weak password. The
|
||||
// "non-common password" (breach) check can only be enforced server-side.
|
||||
|
||||
/// `password_policy.minLength`.
|
||||
final int? passwordMinLength;
|
||||
|
||||
/// `password_policy.enforceUpperLowerCase`.
|
||||
final bool passwordEnforceUpperLower;
|
||||
|
||||
/// `password_policy.enforceNumericCharacters`.
|
||||
final bool passwordEnforceNumeric;
|
||||
|
||||
/// `password_policy.enforceSpecialCharacters`.
|
||||
final bool passwordEnforceSpecial;
|
||||
|
||||
const NextcloudSharingCapabilities({
|
||||
this.apiEnabled = false,
|
||||
this.publicEnabled = false,
|
||||
this.publicMultipleLinks = false,
|
||||
this.publicUploadEnabled = false,
|
||||
this.publicPasswordEnforced = false,
|
||||
this.publicExpireEnabled = false,
|
||||
this.publicExpireDays,
|
||||
this.publicExpireEnforced = false,
|
||||
this.groupEnabled = false,
|
||||
this.resharing = false,
|
||||
this.passwordMinLength,
|
||||
this.passwordEnforceUpperLower = false,
|
||||
this.passwordEnforceNumeric = false,
|
||||
this.passwordEnforceSpecial = false,
|
||||
});
|
||||
|
||||
/// Parses the `files_sharing` sub-map of an OCS `cloud/capabilities`
|
||||
/// response, plus the optional sibling `password_policy` map. Tolerates
|
||||
/// missing intermediate maps and type drift.
|
||||
factory NextcloudSharingCapabilities.fromFilesSharing(
|
||||
Map<String, dynamic> filesSharing, {
|
||||
Map<String, dynamic>? passwordPolicy,
|
||||
}) {
|
||||
Map<String, dynamic>? sub(Map<String, dynamic>? m, String key) {
|
||||
final value = m?[key];
|
||||
return value is Map<String, dynamic> ? value : null;
|
||||
}
|
||||
|
||||
bool boolAt(Map<String, dynamic>? m, String key) => m?[key] == true;
|
||||
int? intAt(Map<String, dynamic>? m, String key) {
|
||||
final v = m?[key];
|
||||
if (v is int) return v;
|
||||
if (v is String) return int.tryParse(v);
|
||||
return null;
|
||||
}
|
||||
|
||||
final public = sub(filesSharing, 'public');
|
||||
final password = sub(public, 'password');
|
||||
final expire = sub(public, 'expire_date');
|
||||
final group = sub(filesSharing, 'group');
|
||||
|
||||
return NextcloudSharingCapabilities(
|
||||
apiEnabled: boolAt(filesSharing, 'api_enabled'),
|
||||
publicEnabled: boolAt(public, 'enabled'),
|
||||
publicMultipleLinks: boolAt(public, 'multiple_links'),
|
||||
publicUploadEnabled: boolAt(public, 'upload'),
|
||||
publicPasswordEnforced: boolAt(password, 'enforced'),
|
||||
publicExpireEnabled: boolAt(expire, 'enabled'),
|
||||
publicExpireDays: intAt(expire, 'days'),
|
||||
publicExpireEnforced: boolAt(expire, 'enforced'),
|
||||
// Newer servers nest it under `group.enabled`; older ones expose a flat
|
||||
// `group_sharing` boolean.
|
||||
groupEnabled:
|
||||
boolAt(group, 'enabled') || boolAt(filesSharing, 'group_sharing'),
|
||||
resharing: boolAt(filesSharing, 'resharing'),
|
||||
passwordMinLength: intAt(passwordPolicy, 'minLength'),
|
||||
passwordEnforceUpperLower: boolAt(
|
||||
passwordPolicy,
|
||||
'enforceUpperLowerCase',
|
||||
),
|
||||
passwordEnforceNumeric: boolAt(
|
||||
passwordPolicy,
|
||||
'enforceNumericCharacters',
|
||||
),
|
||||
passwordEnforceSpecial: boolAt(
|
||||
passwordPolicy,
|
||||
'enforceSpecialCharacters',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user