From a788b5b586d391265e0d4b9ace87a6e9b0a4c965 Mon Sep 17 00:00:00 2001 From: primozratej Date: Sun, 11 Jun 2023 11:25:36 +0400 Subject: [PATCH 1/2] Fix Unreachable HumHub --- lib/models/manifest.dart | 7 +- lib/pages/opener.dart | 84 +++--------------- lib/pages/web_view.dart | 6 +- lib/util/opener_controller.dart | 88 +++++++++++++++++++ pubspec.lock | 146 +++++++++++++++++++++++++++++++- pubspec.yaml | 1 + test/opener_test.dart | 65 ++++++++++++++ test/widget_test.dart | 18 ---- 8 files changed, 319 insertions(+), 96 deletions(-) create mode 100644 lib/util/opener_controller.dart create mode 100644 test/opener_test.dart delete mode 100644 test/widget_test.dart diff --git a/lib/models/manifest.dart b/lib/models/manifest.dart index 6e9c5d1..9c00d4f 100644 --- a/lib/models/manifest.dart +++ b/lib/models/manifest.dart @@ -12,11 +12,8 @@ class Manifest { this.backgroundColor, this.themeColor); String get baseUrl { - int index = startUrl.indexOf("humhub.com"); - if (index != -1) { - return startUrl.substring(0, index + "humhub.com".length); - } - throw Exception("Can't define base url"); + Uri url = Uri.parse(startUrl); + return url.origin; } factory Manifest.fromJson(Map json) { diff --git a/lib/pages/opener.dart b/lib/pages/opener.dart index 0a85bec..197864e 100644 --- a/lib/pages/opener.dart +++ b/lib/pages/opener.dart @@ -1,14 +1,9 @@ -import 'dart:developer'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:humhub/models/hum_hub.dart'; import 'package:humhub/pages/web_view.dart'; -import 'package:humhub/util/api_provider.dart'; import 'package:humhub/util/const.dart'; -import 'package:humhub/util/form_helper.dart'; -import 'package:humhub/models/manifest.dart'; +import 'package:humhub/util/opener_controller.dart'; import 'package:humhub/util/providers.dart'; -import 'package:loggy/loggy.dart'; import 'package:rive/rive.dart'; import 'help/help.dart'; @@ -21,11 +16,7 @@ class Opener extends ConsumerStatefulWidget { } class OpenerState extends ConsumerState { - final helper = FormHelper(); - final String formUrlKey = "redirect_url"; - final String error404 = "404"; - late String? postcodeErrorMessage; - TextEditingController urlTextController = TextEditingController(); + late OpenerController controlLer; late RiveAnimationController _controller; late SimpleAnimation _animation; // Fade out Logo and opener when redirecting @@ -40,6 +31,7 @@ class OpenerState extends ConsumerState { @override Widget build(BuildContext context) { + controlLer = OpenerController(ref: ref); InputDecoration openerDecoration = InputDecoration( focusedBorder: const OutlineInputBorder( borderSide: BorderSide( @@ -62,7 +54,7 @@ class OpenerState extends ConsumerState { body: SafeArea( bottom: false, child: Form( - key: helper.key, + key: controlLer.helper.key, child: Stack( fit: StackFit.expand, children: [ @@ -99,16 +91,16 @@ class OpenerState extends ConsumerState { future: ref.read(humHubProvider).getLastUrl(), builder: (context, snapshot) { if (snapshot.hasData) { - urlTextController.text = snapshot.data!; + controlLer.urlTextController.text = snapshot.data!; return TextFormField( - controller: urlTextController, + controller: controlLer.urlTextController, cursorColor: Theme.of(context).textTheme.bodySmall?.color, - onSaved: helper.onSaved(formUrlKey), + onSaved: controlLer.helper.onSaved(controlLer.formUrlKey), style: const TextStyle( decoration: TextDecoration.none, ), decoration: openerDecoration, - validator: validateUrl, + validator: controlLer.validateUrl, ); } return progress; @@ -134,7 +126,12 @@ class OpenerState extends ConsumerState { width: 140, decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(5)), child: TextButton( - onPressed: onPressed, + onPressed: () { + controlLer.initHumHub(); + ref.read(humHubProvider).getInstance().then((value) { + Navigator.pushNamed(ref.context, WebViewApp.path, arguments: value.manifest); + }); + }, child: Text( 'Connect', style: TextStyle(color: openerColor, fontSize: 20), @@ -196,57 +193,4 @@ class OpenerState extends ConsumerState { ), ); } - - onPressed() async { - // Validate the URL format and if !value.isEmpty - if (!helper.validate()) return; - helper.save(); - // Get the manifest.json for given url. - Uri url = assumeUrl(helper.model[formUrlKey]!); - logInfo("Host: ${url.host}"); - AsyncValue? asyncData; - for (var i = url.pathSegments.length - 1; i >= 0; i--) { - String urlIn = "${url.origin}/${url.pathSegments.getRange(0, i).join('/')}"; - asyncData = await APIProvider.of(ref).request(Manifest.get(i != 0 ? urlIn : url.origin)); - if (!asyncData.hasError) break; - } - if (url.pathSegments.isEmpty) { - asyncData = await APIProvider.of(ref).request(Manifest.get(url.origin)); - } - // If manifest.json does not exist the url is incorrect. - // This is a temp. fix the validator expect sync. function this is some established workaround. - // In the future we could define our own TextFormField that would also validate the API responses. - // But it this is not acceptable I can suggest simple popup or tempPopup. - if (asyncData!.hasError) { - log("Open URL error: $asyncData"); - String value = urlTextController.text; - urlTextController.text = error404; - helper.validate(); - urlTextController.text = value; - } else { - Manifest manifest = asyncData.value!; - // Set the manifestStateProvider with the manifest value so that it's globally accessible - // Generate hash and save it to store - String lastUrl = await ref.read(humHubProvider).getLastUrl(); - String currentUrl = urlTextController.text; - String hash = HumHub.generateHash(32); - if (lastUrl == currentUrl) hash = ref.read(humHubProvider).randomHash ?? hash; - ref.read(humHubProvider).setInstance(HumHub(manifest: manifest, randomHash: hash)); - if (context.mounted) Navigator.pushNamed(context, WebViewApp.path, arguments: manifest); - } - } - - Uri assumeUrl(String url) { - if (url.startsWith("https://") || url.startsWith("http://")) return Uri.parse(url); - return Uri.parse("https://$url"); - } - - String? validateUrl(String? value) { - if (value == error404) return 'Your HumHub installation does not exist'; - - if (value == null || value.isEmpty) { - return 'Specify you HumHub location'; - } - return null; - } } diff --git a/lib/pages/web_view.dart b/lib/pages/web_view.dart index 57bf574..b9b1b4a 100644 --- a/lib/pages/web_view.dart +++ b/lib/pages/web_view.dart @@ -73,7 +73,8 @@ class WebViewAppState extends ConsumerState { ); } - Future _shouldOverrideUrlLoading(InAppWebViewController controller, NavigationAction action) async { + Future _shouldOverrideUrlLoading( + InAppWebViewController controller, NavigationAction action) async { // 1st check if url is not def. app url and open it in a browser or inApp. final url = action.request.url!.origin; if (!url.startsWith(manifest.baseUrl)) { @@ -171,7 +172,8 @@ class WebViewAppState extends ConsumerState { if (url!.path.contains('/user/auth/login')) { webViewController.evaluateJavascript(source: "document.querySelector('#login-rememberme').checked=true"); webViewController.evaluateJavascript( - source: "document.querySelector('#account-login-form > div.form-group.field-login-rememberme').style.display='none';"); + source: + "document.querySelector('#account-login-form > div.form-group.field-login-rememberme').style.display='none';"); } _pullToRefreshController?.endRefreshing(); } diff --git a/lib/util/opener_controller.dart b/lib/util/opener_controller.dart new file mode 100644 index 0000000..0eda0cf --- /dev/null +++ b/lib/util/opener_controller.dart @@ -0,0 +1,88 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:http/http.dart'; +import 'package:humhub/models/hum_hub.dart'; +import 'package:humhub/models/manifest.dart'; +import 'package:humhub/util/providers.dart'; +import 'package:http/http.dart' as http; +import 'api_provider.dart'; +import 'form_helper.dart'; +import 'dart:developer'; + +class OpenerController { + late AsyncValue? asyncData; + bool doesViewExist = false; + final helper = FormHelper(); + TextEditingController urlTextController = TextEditingController(); + late String? postcodeErrorMessage; + final String formUrlKey = "redirect_url"; + final String error404 = "404"; + final WidgetRef ref; + + OpenerController({required this.ref}); + + findManifest(String url) async { + Uri uri = assumeUrl(url); + for (var i = uri.pathSegments.length - 1; i >= 0; i--) { + String urlIn = "${uri.origin}/${uri.pathSegments.getRange(0, i).join('/')}"; + asyncData = await APIProvider.of(ref).request(Manifest.get(i != 0 ? urlIn : uri.origin)); + if (!asyncData!.hasError) break; + } + if (uri.pathSegments.isEmpty) { + asyncData = await APIProvider.of(ref).request(Manifest.get(uri.origin)); + } + checkHumHubModuleView(uri); + } + + checkHumHubModuleView(Uri uri) async { + Response? response; + response = await http.Client().get(uri).catchError((err) {return Response("Found manifest but not humhub.modules.ui.view tag", 404);}); + + doesViewExist = response.statusCode == 200 && response.body.contains('humhub.modules.ui.view'); + } + + initHumHub() async { + // Validate the URL format and if !value.isEmpty + if (!helper.validate()) return; + helper.save(); + // Get the manifest.json for given url. + findManifest(helper.model[formUrlKey]!); + // If manifest.json does not exist the url is incorrect. + // This is a temp. fix the validator expect sync. function this is some established workaround. + // In the future we could define our own TextFormField that would also validate the API responses. + // But it this is not acceptable I can suggest simple popup or tempPopup. + if (asyncData!.hasError || doesViewExist) { + log("Open URL error: $asyncData"); + String value = urlTextController.text; + urlTextController.text = error404; + helper.validate(); + urlTextController.text = value; + } else { + Manifest manifest = asyncData!.value!; + // Set the manifestStateProvider with the manifest value so that it's globally accessible + // Generate hash and save it to store + String lastUrl = ""; + lastUrl = await ref.read(humHubProvider).getLastUrl(); + String currentUrl = urlTextController.text; + String hash = HumHub.generateHash(32); + if (lastUrl == currentUrl) hash = ref.read(humHubProvider).randomHash ?? hash; + ref.read(humHubProvider).setInstance(HumHub(manifest: manifest, randomHash: hash)); + } + } + + + + Uri assumeUrl(String url) { + if (url.startsWith("https://") || url.startsWith("http://")) return Uri.parse(url); + return Uri.parse("https://$url"); + } + + String? validateUrl(String? value) { + if (value == error404) return 'Your HumHub installation does not exist'; + + if (value == null || value.isEmpty) { + return 'Specify you HumHub location'; + } + return null; + } +} diff --git a/pubspec.lock b/pubspec.lock index e56f359..a263c82 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,14 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: ae92f5d747aee634b87f89d9946000c2de774be1d6ac3e58268224348cd0101a + url: "https://pub.dev" + source: hosted + version: "61.0.0" _flutterfire_internals: dependency: transitive description: @@ -9,6 +17,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.15" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: ea3d8652bda62982addfd92fdc2d0214e5f82e43325104990d4f4c4a2a313562 + url: "https://pub.dev" + source: hosted + version: "5.13.0" app_settings: dependency: "direct main" description: @@ -49,6 +65,30 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + build: + dependency: transitive + description: + name: build + sha256: "43865b79fbb78532e4bff7c33087aa43b1d488c4fdef014eaef568af6d8016dc" + url: "https://pub.dev" + source: hosted + version: "2.4.0" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: "598a2a682e2a7a90f08ba39c0aaa9374c5112340f0a2e275f61b59389543d166" + url: "https://pub.dev" + source: hosted + version: "8.6.1" characters: dependency: transitive description: @@ -65,6 +105,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + code_builder: + dependency: transitive + description: + name: code_builder + sha256: "4ad01d6e56db961d29661561effde45e519939fdaeb46c351275b182eac70189" + url: "https://pub.dev" + source: hosted + version: "4.5.0" collection: dependency: transitive description: @@ -73,6 +121,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.17.0" + convert: + dependency: transitive + description: + name: convert + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + crypto: + dependency: transitive + description: + name: crypto + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + url: "https://pub.dev" + source: hosted + version: "3.0.3" cupertino_icons: dependency: "direct main" description: @@ -81,6 +145,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.5" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: f4f1f73ab3fd2afcbcca165ee601fe980d966af6a21b5970c6c9376955c528ad + url: "https://pub.dev" + source: hosted + version: "2.3.1" dbus: dependency: transitive description: @@ -169,6 +241,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.2.14" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + url: "https://pub.dev" + source: hosted + version: "1.1.0" flutter: dependency: "direct main" description: flutter @@ -296,6 +376,14 @@ packages: description: flutter source: sdk version: "0.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + url: "https://pub.dev" + source: hosted + version: "2.1.2" graphs: dependency: transitive description: @@ -336,6 +424,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.1" + logging: + dependency: transitive + description: + name: logging + sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + url: "https://pub.dev" + source: hosted + version: "1.2.0" loggy: dependency: "direct main" description: @@ -368,6 +464,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.8.0" + mockito: + dependency: "direct main" + description: + name: mockito + sha256: dd61809f04da1838a680926de50a9e87385c1de91c6579629c3d1723946e8059 + url: "https://pub.dev" + source: hosted + version: "5.4.0" + package_config: + dependency: transitive + description: + name: package_config + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" package_info_plus: dependency: "direct main" description: @@ -464,6 +576,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.2.4" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + url: "https://pub.dev" + source: hosted + version: "2.1.4" receive_sharing_intent: dependency: "direct main" description: @@ -493,6 +613,14 @@ packages: description: flutter source: sdk version: "0.0.99" + source_gen: + dependency: transitive + description: + name: source_gen + sha256: "373f96cf5a8744bc9816c1ff41cf5391bbdbe3d7a96fe98c622b6738a8a7bd33" + url: "https://pub.dev" + source: hosted + version: "1.3.2" source_span: dependency: transitive description: @@ -661,6 +789,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + watcher: + dependency: transitive + description: + name: watcher + sha256: "6a7f46926b01ce81bfc339da6a7f20afbe7733eff9846f6d6a5466aa4c6667c0" + url: "https://pub.dev" + source: hosted + version: "1.0.2" webview_flutter: dependency: "direct main" description: @@ -717,6 +853,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.0" + yaml: + dependency: transitive + description: + name: yaml + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + url: "https://pub.dev" + source: hosted + version: "3.1.2" sdks: - dart: ">=2.18.6 <3.0.0" + dart: ">=2.19.0 <3.0.0" flutter: ">=3.0.1" diff --git a/pubspec.yaml b/pubspec.yaml index 1deba69..abcb08c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -56,6 +56,7 @@ dependencies: flutter_app_badger: ^1.5.0 rive: 0.9.0 auto_size_text: ^3.0.0 + mockito: ^5.4.0 dev_dependencies: flutter_test: diff --git a/test/opener_test.dart b/test/opener_test.dart new file mode 100644 index 0000000..fba10a1 --- /dev/null +++ b/test/opener_test.dart @@ -0,0 +1,65 @@ +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:humhub/pages/opener.dart'; +import 'package:mockito/mockito.dart'; + +class MyHttpOverrides extends HttpOverrides { + @override + HttpClient createHttpClient(SecurityContext? context) { + return super.createHttpClient(context) + ..badCertificateCallback = (X509Certificate cert, String host, int port) => true; + } +} + +class MockFlutterSecureStorage extends Mock implements FlutterSecureStorage {} + +Future main() async { + setUp(() { + HttpOverrides.global = MyHttpOverrides(); + }); + + testWidgets('Test opener URL parsing', (WidgetTester tester) async { + // Key value map of URLs with bool that represent the expected value + Map urlsAndValuesIn = { + "https://community.humhub.com": true, + "https://demo.cuzy.app/": true, + "https://sometestproject12345.humhub.com/": true, + "https://sometestproject12345.humhub.com/some/path": true, + "https://sometestproject123456.humhub.com/": false, + "https://sometestproject123456.humhubb.com": false, + "sometestproject12345.humhub.com": true, + "//demo.cuzy.app/": false, + }; + + Map urlsAndValuesOut = {}; + Key openerKey = const Key('opener'); + + for (var urlEntry in urlsAndValuesIn.entries) { + String url = urlEntry.key; + await tester.pumpWidget( + MaterialApp( + home: ProviderScope( + child: Scaffold(body: Opener(key: openerKey)), + ), + ), + ); + final state = tester.state(find.byKey(openerKey)); + state.controlLer.helper.model[state.controlLer.formUrlKey] = url; + bool isBreaking = false; + + await tester.runAsync(() async { + try { + await state.controlLer.findManifest(url); + } catch (er) { + isBreaking = true; + } + }); + isBreaking ? urlsAndValuesOut[url] = !isBreaking : urlsAndValuesOut[url] = !state.controlLer.asyncData!.hasError; + } + + expect(urlsAndValuesOut, urlsAndValuesIn); + }); +} diff --git a/test/widget_test.dart b/test/widget_test.dart deleted file mode 100644 index 388ffd2..0000000 --- a/test/widget_test.dart +++ /dev/null @@ -1,18 +0,0 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility in the flutter_test package. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - -import 'package:flutter_test/flutter_test.dart'; - -import 'package:humhub/main.dart'; - -void main() { - testWidgets('Test that always pass for GitHub Actions flutter test case', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(const MyApp()); - expect(true, true); - }); -} From 8a92513c4bbfdae30eeb39498388220a8cf14bd0 Mon Sep 17 00:00:00 2001 From: primozratej Date: Sun, 11 Jun 2023 13:14:06 +0400 Subject: [PATCH 2/2] Change to async --- lib/pages/opener.dart | 4 ++-- lib/util/opener_controller.dart | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/pages/opener.dart b/lib/pages/opener.dart index 197864e..53f06af 100644 --- a/lib/pages/opener.dart +++ b/lib/pages/opener.dart @@ -126,8 +126,8 @@ class OpenerState extends ConsumerState { width: 140, decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(5)), child: TextButton( - onPressed: () { - controlLer.initHumHub(); + onPressed: () async { + await controlLer.initHumHub(); ref.read(humHubProvider).getInstance().then((value) { Navigator.pushNamed(ref.context, WebViewApp.path, arguments: value.manifest); }); diff --git a/lib/util/opener_controller.dart b/lib/util/opener_controller.dart index 0eda0cf..e5cd475 100644 --- a/lib/util/opener_controller.dart +++ b/lib/util/opener_controller.dart @@ -46,7 +46,7 @@ class OpenerController { if (!helper.validate()) return; helper.save(); // Get the manifest.json for given url. - findManifest(helper.model[formUrlKey]!); + await findManifest(helper.model[formUrlKey]!); // If manifest.json does not exist the url is incorrect. // This is a temp. fix the validator expect sync. function this is some established workaround. // In the future we could define our own TextFormField that would also validate the API responses.