A framework for building convergent cross-platform Nextcloud clients using Flutter.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

219 lines
6.3 KiB

import 'dart:convert';
import 'package:collection/collection.dart';
import 'package:crypto/crypto.dart';
import 'package:flutter/foundation.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:meta/meta.dart';
import 'package:nextcloud/nextcloud.dart';
part 'account.g.dart';
/// Credentials interface
@internal
@immutable
abstract interface class Credentials {
/// Url of the server
abstract final Uri serverURL;
/// Username
abstract final String username;
/// App password
abstract final String? password;
}
@JsonSerializable()
@immutable
class Account implements Credentials {
Account({
required this.serverURL,
required this.username,
this.password,
this.userAgent,
}) : client = NextcloudClient(
serverURL,
loginName: username,
password: password,
appPassword: password,
userAgentOverride: userAgent,
cookieJar: !kIsWeb ? CookieJar() : null,
);
factory Account.fromJson(final Map<String, dynamic> json) => _$AccountFromJson(json);
Map<String, dynamic> toJson() => _$AccountToJson(this);
@override
final Uri serverURL;
@override
final String username;
@override
final String? password;
final String? userAgent;
@override
bool operator ==(final Object other) =>
other is Account &&
other.serverURL == serverURL &&
other.username == username &&
other.password == password &&
other.userAgent == userAgent;
@override
int get hashCode => serverURL.hashCode + username.hashCode;
final NextcloudClient client;
String get id {
final key = '$username@$serverURL';
return _idCache[key] ??= sha1.convert(utf8.encode(key)).toString();
}
/// A human readable representation of [username] and [serverURL].
String get humanReadableID {
// Maybe also show path if it is not '/' ?
final buffer = StringBuffer()
..write(username)
..write('@')
..write(serverURL.host);
if (serverURL.hasPort) {
buffer
..write(':')
..write(serverURL.port);
}
return buffer.toString();
}
/// Completes an incomplete [Uri] using the [serverURL].
///
/// Some Nextcloud APIs return [Uri]s to resources on the server (e.g. an image) but only give an absolute path.
/// Those [Uri]s need to be completed using the [serverURL] to have a working [Uri].
///
/// The paths of the [serverURL] and the [uri] need to be join to get the full path, unless the [uri] path is already an absolute path.
/// In that case an instance hosted at a sub folder will already contain the sub folder part in the [uri].
Uri completeUri(final Uri uri) {
final result = serverURL.resolveUri(uri);
if (!uri.hasAbsolutePath) {
return result.replace(path: '${serverURL.path}/${uri.path}');
}
return result;
}
/// Removes the [serverURL] part from the [uri].
///
/// Should be used when trying to push a [uri] from an API to the router as it might contain the scheme, host and sub path of the instance which will not work with the router.
Uri stripUri(final Uri uri) => Uri.parse(uri.toString().replaceFirst(serverURL.toString(), ''));
}
Map<String, String> _idCache = {};
extension AccountFind on Iterable<Account> {
Account? tryFind(final String? accountID) => firstWhereOrNull((final account) => account.id == accountID);
Account find(final String accountID) => firstWhere((final account) => account.id == accountID);
}
/// QRcode Login credentials.
///
/// The Credentials as provided by the server when manually creating an app
/// password.
@internal
@immutable
class LoginQRcode implements Credentials {
/// Creates a new LoginQRcode object.
@visibleForTesting
const LoginQRcode({
required this.serverURL,
required this.username,
required this.password,
});
@override
final Uri serverURL;
@override
final String username;
@override
final String password;
/// Pattern matching the full QRcode content.
static final _loginQRcodeUrlRegex = RegExp(r'^nc://login/user:(.*)&password:(.*)&server:(.*)$');
/// Pattern matching the path part of the QRcode.
///
/// This is used when launching the app through an intent.
static final _loginQRcodePathRegex = RegExp(r'^/user:(.*)&password:(.*)&server:(.*)$');
/// Creates a new `LoginQRcode` object by parsing a url string.
///
/// If the [url] string is not valid as a LoginQRcode a [FormatException] is
/// thrown.
///
/// Example:
/// ```dart
/// final loginQRcode =
/// LoginQRcode.parse('nc://login/user:JohnDoe&password:super_secret&server:example.com');
/// print(loginQRcode.serverURL); // JohnDoe
/// print(loginQRcode.username); // super_secret
/// print(loginQRcode.password); // example.com
///
/// LoginQRcode.parse('::Not valid LoginQRcode::'); // Throws FormatException.
/// ```
static LoginQRcode parse(final String url) {
for (final regex in [_loginQRcodeUrlRegex, _loginQRcodePathRegex]) {
final matches = regex.allMatches(url);
if (matches.isEmpty) {
continue;
}
final match = matches.single;
if (match.groupCount != 3) {
continue;
}
return LoginQRcode(
serverURL: Uri.parse(match.group(3)!),
username: match.group(1)!,
password: match.group(2)!,
);
}
throw const FormatException();
}
/// Creates a new `LoginQRcode` object by parsing a url string.
///
/// Returns `null` if the [url] string is not valid as a LoginQRcode.
///
/// Example:
/// ```dart
/// final loginQRcode =
/// LoginQRcode.parse('nc://login/user:JohnDoe&password:super_secret&server:example.com');
/// print(loginQRcode.serverURL); // JohnDoe
/// print(loginQRcode.username); // super_secret
/// print(loginQRcode.password); // example.com
///
/// final notLoginQRcode = LoginQRcode.tryParse('::Not valid LoginQRcode::');
/// print(notLoginQRcode); // null
/// ```
static LoginQRcode? tryParse(final String url) {
try {
return parse(url);
} on FormatException {
return null;
}
}
@override
bool operator ==(final Object other) =>
other is LoginQRcode && other.serverURL == serverURL && other.username == username && other.password == password;
@override
int get hashCode => Object.hashAll([
serverURL,
username,
password,
]);
}