소스 검색

Merge pull request #2282 from nextcloud/CalDAVCardDAV

Automatically set up sync for Calendar and Contacts
Marino Faggiana 2 년 전
부모
커밋
88ebce09b3

+ 1 - 0
.swiftlint.yml

@@ -113,6 +113,7 @@ excluded:
   - iOSClient/Networking/NCNetworkingProcessUpload.swift
   - iOSClient/Networking/NCOperationQueue.swift
   - iOSClient/Networking/NCService.swift
+  - iOSClient/Networking/NCConfigServer.swift
   - iOSClient/Notification/NCNotification.swift
   - iOSClient/Recent/NCRecent.swift
   - iOSClient/Rename file/NCRenameFile.swift

+ 21 - 0
Nextcloud.xcodeproj/project.pbxproj

@@ -318,6 +318,8 @@
 		F77BB746289984CA0090FC19 /* UIViewController+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F77BB745289984CA0090FC19 /* UIViewController+Extension.swift */; };
 		F77BB748289985270090FC19 /* UITabBarController+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F77BB747289985270090FC19 /* UITabBarController+Extension.swift */; };
 		F77BB74A2899857B0090FC19 /* UINavigationController+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F77BB7492899857B0090FC19 /* UINavigationController+Extension.swift */; };
+		F77BC3EB293E5268005F2B08 /* Swifter in Frameworks */ = {isa = PBXBuildFile; productRef = F77BC3EA293E5268005F2B08 /* Swifter */; };
+		F77BC3ED293E528A005F2B08 /* NCConfigServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F77BC3EC293E528A005F2B08 /* NCConfigServer.swift */; };
 		F77ED59128C9CE9D00E24ED0 /* ToolbarData.swift in Sources */ = {isa = PBXBuildFile; fileRef = F77ED59028C9CE9D00E24ED0 /* ToolbarData.swift */; };
 		F77ED59328C9CEA000E24ED0 /* ToolbarWidgetProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F77ED59228C9CEA000E24ED0 /* ToolbarWidgetProvider.swift */; };
 		F77ED59528C9CEA400E24ED0 /* ToolbarWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F77ED59428C9CEA300E24ED0 /* ToolbarWidgetView.swift */; };
@@ -922,6 +924,7 @@
 		F77BB745289984CA0090FC19 /* UIViewController+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIViewController+Extension.swift"; sourceTree = "<group>"; };
 		F77BB747289985270090FC19 /* UITabBarController+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UITabBarController+Extension.swift"; sourceTree = "<group>"; };
 		F77BB7492899857B0090FC19 /* UINavigationController+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UINavigationController+Extension.swift"; sourceTree = "<group>"; };
+		F77BC3EC293E528A005F2B08 /* NCConfigServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCConfigServer.swift; sourceTree = "<group>"; };
 		F77ED59028C9CE9D00E24ED0 /* ToolbarData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolbarData.swift; sourceTree = "<group>"; };
 		F77ED59228C9CEA000E24ED0 /* ToolbarWidgetProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolbarWidgetProvider.swift; sourceTree = "<group>"; };
 		F77ED59428C9CEA300E24ED0 /* ToolbarWidgetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolbarWidgetView.swift; sourceTree = "<group>"; };
@@ -1238,6 +1241,7 @@
 				F76DA941277B75870082465B /* KTVHTTPCache.xcframework in Frameworks */,
 				F7ED547C25EEA65400956C55 /* QRCodeReader in Frameworks */,
 				F788ECC7263AAAFA00ADC67F /* MarkdownKit in Frameworks */,
+				F77BC3EB293E5268005F2B08 /* Swifter in Frameworks */,
 				F7BB7E4727A18C56009B9F29 /* Parchment in Frameworks */,
 				F734B06628E75C0100E180D5 /* TLPhotoPicker in Frameworks */,
 				F770768E263A8C3400A1BA94 /* FloatingPanel in Frameworks */,
@@ -1566,6 +1570,7 @@
 				F70D8D8024A4A9BF000A5756 /* NCNetworkingProcessUpload.swift */,
 				F72A47EB2487B06B005AD489 /* NCOperationQueue.swift */,
 				F755BD9A20594AC7008C5FBB /* NCService.swift */,
+				F77BC3EC293E528A005F2B08 /* NCConfigServer.swift */,
 			);
 			path = Networking;
 			sourceTree = "<group>";
@@ -2446,6 +2451,7 @@
 				F72AD70C28C24B93006CB92D /* NextcloudKit */,
 				F734B06528E75C0100E180D5 /* TLPhotoPicker */,
 				F77333872927A72100466E35 /* OpenSSL */,
+				F77BC3EA293E5268005F2B08 /* Swifter */,
 			);
 			productName = "Crypto Cloud";
 			productReference = F7CE8AFA1DC1F8D8009CAE48 /* Nextcloud.app */;
@@ -2603,6 +2609,7 @@
 				F783034028B511D200B84583 /* XCRemoteSwiftPackageReference "NextcloudKit" */,
 				F734B06428E75C0100E180D5 /* XCRemoteSwiftPackageReference "TLPhotoPicker" */,
 				F77333862927A72100466E35 /* XCRemoteSwiftPackageReference "OpenSSL" */,
+				F77BC3E9293E5268005F2B08 /* XCRemoteSwiftPackageReference "swifter" */,
 			);
 			productRefGroup = F7F67B9F1A24D27800EE80DA;
 			projectDirPath = "";
@@ -3124,6 +3131,7 @@
 				F75D19E325EFE09000D74598 /* NCTrash+Menu.swift in Sources */,
 				F70CAE3A1F8CF31A008125FD /* NCEndToEndEncryption.m in Sources */,
 				AF93471B27E2361E002537EE /* NCShareAdvancePermission.swift in Sources */,
+				F77BC3ED293E528A005F2B08 /* NCConfigServer.swift in Sources */,
 				F70753EB2542A99800972D44 /* NCViewerMediaPage.swift in Sources */,
 				F7FAFD3A28BFA948000777FE /* NCNotification+Menu.swift in Sources */,
 				F74C0436253F1CDC009762AB /* NCShares.swift in Sources */,
@@ -3989,6 +3997,14 @@
 				minimumVersion = 1.0.0;
 			};
 		};
+		F77BC3E9293E5268005F2B08 /* XCRemoteSwiftPackageReference "swifter" */ = {
+			isa = XCRemoteSwiftPackageReference;
+			repositoryURL = "https://github.com/httpswift/swifter.git";
+			requirement = {
+				branch = stable;
+				kind = branch;
+			};
+		};
 		F783034028B511D200B84583 /* XCRemoteSwiftPackageReference "NextcloudKit" */ = {
 			isa = XCRemoteSwiftPackageReference;
 			repositoryURL = "https://github.com/nextcloud/NextcloudKit";
@@ -4204,6 +4220,11 @@
 			package = F77333862927A72100466E35 /* XCRemoteSwiftPackageReference "OpenSSL" */;
 			productName = OpenSSL;
 		};
+		F77BC3EA293E5268005F2B08 /* Swifter */ = {
+			isa = XCSwiftPackageProductDependency;
+			package = F77BC3E9293E5268005F2B08 /* XCRemoteSwiftPackageReference "swifter" */;
+			productName = Swifter;
+		};
 		F783030428B4C50600B84583 /* SVGKit */ = {
 			isa = XCSwiftPackageProductDependency;
 			package = F75E57A725BF0D61002B72C2 /* XCRemoteSwiftPackageReference "SVGKit" */;

+ 12 - 0
iOSClient/Images.xcassets/caldavcardav.imageset/Contents.json

@@ -0,0 +1,12 @@
+{
+  "images" : [
+    {
+      "filename" : "icons8-calendario-delle-persone.svg",
+      "idiom" : "universal"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

+ 1 - 0
iOSClient/Images.xcassets/caldavcardav.imageset/icons8-calendario-delle-persone.svg

@@ -0,0 +1 @@
+<svg fill="#000000" xmlns="http://www.w3.org/2000/svg"  viewBox="0 0 50 50" width="50px" height="50px"><path d="M48.6 32H46v-1c0-.6-.4-1-1-1s-1 .4-1 1v1h-8v-1c0-.6-.4-1-1-1s-1 .4-1 1v1h-2.6c-.8 0-1.4.6-1.4 1.4v15.3c0 .7.6 1.3 1.4 1.3h17.3c.8 0 1.3-.6 1.3-1.4V33.4C49.9 32.6 49.4 32 48.6 32zM34 34c0 .6.4 1 1 1s1-.4 1-1h8c0 .6.4 1 1 1s1-.4 1-1h2v2H32v-2H34zM35 40h2v2h-2V40zM39 40h2v2h-2V40zM43 40h2v2h-2V40zM35 44h2v2h-2V44zM39 44h2v2h-2V44zM43 44h2v2h-2V44zM30.952 7.823C30.1 6.414 28.8 5.527 27.083 5.185 26.117 3.965 24.301 3.3 21.879 3.3h-.001c-3.685.083-6.378 1.21-8.005 3.35-1.926 2.533-2.287 6.416-1.076 11.546-.413.531-.785 1.38-.688 2.54.296 2.159 1.116 3.05 1.785 3.415.308 1.749 1.156 3.676 2.007 4.597v3.561c-.611 1.376-2.601 2.172-4.892 3.089-3.864 1.546-8.673 3.47-9.007 9.549L1.943 46H28V28.747c.846-.916 1.69-2.827 2.001-4.569.712-.351 1.59-1.241 1.795-3.494.095-1.126-.215-1.954-.641-2.487C32.004 15.468 32.749 10.794 30.952 7.823zM29.059 18.164l-.316.824.793.396c.089.064.329.409.269 1.125C29.669 21.988 29.2 22.383 29.1 22.4H28.19l-.086.905c-.181 1.896-1.239 3.854-1.6 4.126L26 27.719V32.5c-.018.373-1.464 1.5-4 1.5-2.489 0-4-1.042-4-1.5 0-.152-.039-.292-.1-.421V28.3l.02-.568-.504-.288c-.381-.284-1.439-2.242-1.621-4.138l-.012-.896h-.909c-.143-.056-.59-.53-.778-1.894-.069-.838.359-1.185.358-1.185l.59-.392-.174-.687c-1.213-4.783-1.012-8.28.596-10.395 1.239-1.63 3.412-2.491 6.435-2.56 1.891 0 3.252.476 3.736 1.304l.244.419L26.36 7.09c1.332.188 2.274.766 2.881 1.769C30.689 11.254 29.926 15.735 29.059 18.164z"/></svg>

+ 205 - 0
iOSClient/Networking/NCConfigServer.swift

@@ -0,0 +1,205 @@
+//
+//  NCConfigServer.swift
+//  Nextcloud
+//
+//  Created by Marino Faggiana on 05/12/22.
+//  Copyright © 2022 Marino Faggiana. All rights reserved.
+//
+//  Author Marino Faggiana <marino.faggiana@nextcloud.com>
+//
+//  This program is free software: you can redistribute it and/or modify
+//  it under the terms of the GNU General Public License as published by
+//  the Free Software Foundation, either version 3 of the License, or
+//  (at your option) any later version.
+//
+//  This program is distributed in the hope that it will be useful,
+//  but WITHOUT ANY WARRANTY; without even the implied warranty of
+//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//  GNU General Public License for more details.
+//
+//  You should have received a copy of the GNU General Public License
+//  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+//
+
+import Foundation
+import Swifter
+import NextcloudKit
+
+// Source:
+// https://stackoverflow.com/questions/2338035/installing-a-configuration-profile-on-iphone-programmatically
+
+@objc class NCConfigServer: NSObject, UIActionSheetDelegate, URLSessionDelegate {
+
+    // Start service
+    @objc func startService(url: URL) {
+
+        let defaultSessionConfiguration = URLSessionConfiguration.default
+        let defaultSession = URLSession(configuration: defaultSessionConfiguration, delegate: self, delegateQueue: nil)
+
+        var urlRequest = URLRequest(url: url)
+        urlRequest.headers = NKCommon.shared.getStandardHeaders(nil, customUserAgent: nil)
+
+        let dataTask = defaultSession.dataTask(with: urlRequest) { (data, response, error) in
+            if let error = error {
+                let error = NKError(errorCode: error._code, errorDescription: error.localizedDescription)
+                NCContentPresenter.shared.showInfo(error: error)
+            } else if let data = data {
+                DispatchQueue.main.async { self.start(data: data) }
+            }
+        }
+        dataTask.resume()
+    }
+
+    func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
+        DispatchQueue.global().async {
+            NCNetworking.shared.checkTrustedChallenge(session, didReceive: challenge, completionHandler: completionHandler)
+        }
+    }
+
+    private enum ConfigState: Int {
+        case Stopped, Ready, InstalledConfig, BackToApp
+    }
+
+    internal let listeningPort: in_port_t = 8080
+    internal var configName: String = "Profile install"
+    private var localServer: HttpServer?
+    private var returnURL: String = ""
+    private var configData: Data?
+
+    private var serverState: ConfigState = .Stopped
+    private var registeredForNotifications = false
+    private var backgroundTask = UIBackgroundTaskIdentifier.invalid
+
+    deinit {
+        unregisterFromNotifications()
+    }
+
+    // MARK: - Control functions
+
+    internal func start(data: Data) {
+        self.configData = data
+        self.localServer = HttpServer()
+        self.setupHandlers()
+
+        let page = self.baseURL(pathComponent: "install/")
+        let url = URL(string: page)!
+        if UIApplication.shared.canOpenURL(url as URL) {
+            do {
+                try localServer?.start(listeningPort, forceIPv4: false, priority: .default)
+                serverState = .Ready
+                registerForNotifications()
+                UIApplication.shared.open(url)
+            } catch {
+                let error = NKError(errorCode: error._code, errorDescription: error.localizedDescription)
+                NCContentPresenter.shared.showInfo(error: error)
+                self.stop()
+            }
+        }
+    }
+
+    internal func stop() {
+        if serverState != .Stopped {
+            serverState = .Stopped
+            unregisterFromNotifications()
+        }
+    }
+
+    // MARK: - Private functions
+
+    private func setupHandlers() {
+        localServer?["/install"] = { request in
+            switch self.serverState {
+            case .Stopped:
+                return .notFound()
+            case .Ready:
+                self.serverState = .InstalledConfig
+                return HttpResponse.raw(200, "OK", ["Content-Type": "application/x-apple-aspen-config"], { writer in
+                    do {
+                        if let configData = self.configData {
+                            try writer.write(configData)
+                        }
+                    } catch {
+                        print("Failed to write response data")
+                    }
+                })
+            case .InstalledConfig:
+                return .movedPermanently(self.returnURL)
+            case .BackToApp:
+                let page = self.basePage(pathComponent: nil)
+                return .ok(.html(page))
+            }
+        }
+    }
+
+    private func baseURL(pathComponent: String?) -> String {
+        var page = "http://localhost:\(listeningPort)"
+        if let component = pathComponent {
+            page += "/\(component)"
+        }
+        return page
+    }
+
+    private func basePage(pathComponent: String?) -> String {
+        var page = "<!doctype html><html>" + "<head><meta charset='utf-8'><title>\(self.configName)</title></head>"
+        if let component = pathComponent {
+            let script = "function load() { window.location.href='\(self.baseURL(pathComponent: component))'; } window.setInterval(load, 800);"
+            page += "<script>\(script)</script>"
+        }
+        page += "<body></body></html>"
+        return page
+    }
+
+    private func returnedToApp() {
+        if serverState != .Stopped {
+            serverState = .BackToApp
+            localServer?.stop()
+        }
+    }
+
+    private func registerForNotifications() {
+        if !registeredForNotifications {
+            let notificationCenter = NotificationCenter.default
+            notificationCenter.addObserver(self, selector: #selector(didEnterBackground), name: UIApplication.didEnterBackgroundNotification, object: nil)
+            notificationCenter.addObserver(self, selector: #selector(willEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
+            registeredForNotifications = true
+        }
+    }
+
+    private func unregisterFromNotifications() {
+        if registeredForNotifications {
+            let notificationCenter = NotificationCenter.default
+            notificationCenter.removeObserver(self, name: UIApplication.didEnterBackgroundNotification, object: nil)
+            notificationCenter.removeObserver(self, name: UIApplication.willEnterForegroundNotification, object: nil)
+            registeredForNotifications = false
+        }
+    }
+
+    @objc internal func didEnterBackground(notification: NSNotification) {
+        if serverState != .Stopped {
+            startBackgroundTask()
+        }
+    }
+
+    @objc internal func willEnterForeground(notification: NSNotification) {
+        if backgroundTask != UIBackgroundTaskIdentifier.invalid {
+            stopBackgroundTask()
+            returnedToApp()
+        }
+    }
+
+    private func startBackgroundTask() {
+        let application = UIApplication.shared
+        backgroundTask = application.beginBackgroundTask(expirationHandler: {
+            DispatchQueue.main.async {
+                self.stopBackgroundTask()
+            }
+        })
+    }
+
+    private func stopBackgroundTask() {
+        if backgroundTask != UIBackgroundTaskIdentifier.invalid {
+            UIApplication.shared.endBackgroundTask(self.backgroundTask)
+            backgroundTask = UIBackgroundTaskIdentifier.invalid
+        }
+    }
+}

+ 1 - 1
iOSClient/Networking/NCNetworking.swift

@@ -155,7 +155,7 @@ import Photos
 
     // MARK: - Pinning check
 
-    private func checkTrustedChallenge(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
+    public func checkTrustedChallenge(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
 
         let protectionSpace: URLProtectionSpace = challenge.protectionSpace
         let directoryCertificate = CCUtility.getDirectoryCerificates()!

+ 28 - 3
iOSClient/Settings/NCSettings.m

@@ -49,8 +49,7 @@
     XLFormDescriptor *form = [XLFormDescriptor formDescriptor];
     XLFormSectionDescriptor *section;
     XLFormRowDescriptor *row;
-    //NSInteger serverVersionMajor = [[NCManageDatabase shared] getCapabilitiesServerIntWithAccount:appDelegate.account elements:NCElementsJSON.shared.capabilitiesVersionMajor];
-    
+
     form.rowNavigationOptions = XLFormRowNavigationOptionNone;
     
     // Section AUTO UPLOAD OF CAMERA IMAGES ----------------------------
@@ -99,7 +98,21 @@
     [row.cellConfig setObject:[UIFont systemFontOfSize:15.0] forKey:@"textLabel.font"];
     [row.cellConfig setObject:UIColor.labelColor forKey:@"textLabel.textColor"];
     [section addFormRow:row];
-    
+
+    // Section : CALDAV CARDAV --------------------------------------------------------------
+
+    section = [XLFormSectionDescriptor formSectionWithTitle:NSLocalizedString(@"_calendar_contacts_", nil)];
+    [form addFormSection:section];
+
+    row = [XLFormRowDescriptor formRowDescriptorWithTag:@"caldavcardav" rowType:XLFormRowDescriptorTypeButton title:NSLocalizedString(@"_mobile_config_", nil)];
+    row.cellConfigAtConfigure[@"backgroundColor"] = UIColor.secondarySystemGroupedBackgroundColor;
+    [row.cellConfig setObject:[UIFont systemFontOfSize:15.0] forKey:@"textLabel.font"];
+    [row.cellConfig setObject:@(NSTextAlignmentLeft) forKey:@"textLabel.textAlignment"];
+    [row.cellConfig setObject:UIColor.labelColor forKey:@"textLabel.textColor"];
+    [row.cellConfig setObject:[[UIImage imageNamed:@"caldavcardav"] imageWithColor:NCBrandColor.shared.gray size:25] forKey:@"imageView.image"];
+    row.action.formSelector = @selector(CalDAVCardDAV:);
+    [section addFormRow:row];
+
     // Section : E2EEncryption --------------------------------------------------------------
 
     BOOL isE2EEEnabled = [[NCManageDatabase shared] getCapabilitiesServerBoolWithAccount:appDelegate.account elements:NCElementsJSON.shared.capabilitiesE2EEEnabled exists:false];
@@ -331,6 +344,16 @@
     [self presentViewController:browserWebVC animated:YES completion:nil];
 }
 
+- (void)CalDAVCardDAV:(XLFormRowDescriptor *)sender
+{
+    [self deselectFormRow:sender];
+
+    NSString *url = [appDelegate.urlBase stringByAppendingString:@"/remote.php/dav/provisioning/apple-provisioning.mobileconfig"];
+    NCConfigServer *configServer = [NCConfigServer new];
+    [configServer startServiceWithUrl:[NSURL URLWithString: url]];
+}
+
+
 #pragma mark - Passcode
 
 - (void)didPerformBiometricValidationRequestInPasscodeViewController:(TOPasscodeViewController *)passcodeViewController
@@ -427,6 +450,8 @@
 
     if (section == 1) {
         sectionName = NSLocalizedString(@"_lock_protection_no_screen_footer_", nil);
+    } else if (section == 2) {
+        sectionName = NSLocalizedString(@"_calendar_contacts_footer_", nil);
     } else if (section == numSections) {
         NSString *versionServer = [[NCManageDatabase shared] getCapabilitiesServerStringWithAccount:appDelegate.account elements:NCElementsJSON.shared.capabilitiesVersionString];
         NSString *themingName = [[NCManageDatabase shared] getCapabilitiesServerStringWithAccount:appDelegate.account elements:NCElementsJSON.shared.capabilitiesThemingName];

+ 4 - 1
iOSClient/Supporting Files/en.lproj/Localizable.strings

@@ -897,7 +897,7 @@
 "_keep_running_"            = "Keep the app running for a better user experience";
 "_recent_activity_"         = "Recent activity";
 "_title_lockscreenwidget_"  = "Status";
-"_description_lockscreenwidget_" = "Keep an eye on available space and recent activity";
+"_description_lockscreenwidget_"    = "Keep an eye on available space and recent activity";
 "_no_items_"                = "No items";
 "_check_back_later_"        = "Check back later";
 "_exporting_video_"         = "Exporting video … Tap to cancel.";
@@ -909,6 +909,9 @@
 "_change_upload_filename_"  = "Do you want to save the file with a different name ?";
 "_save_file_as_"            = "Save file as %@";
 "_password_ascii_"          = "The password cannot contain special characters";
+"_calendar_contacts_"       = "Calendar and Contacts";
+"_mobile_config_"           = "Download the configuration profile";
+"_calendar_contacts_footer_"    = "After the downloading you need to go to the Settings app to install it. If a profile is not installed within 8 minutes of downloading it, it is automatically deleted";
 
 // Video
 "_select_trace_"            = "Select the trace";