version bump
[ShaarliOS.git] / swift4 / ShaarliOS / ShaarliM.swift
blobb151dd5c4abf7f8eaf742aa77c580d191e49eab6
1 //
2 //  ShaarliM.swift
3 //  ShaarliOS
4 //
5 //  Created by Marcus Rohrmoser on 21.02.20.
6 //  Copyright © 2019-2022 Marcus Rohrmoser mobile Software http://mro.name/me. All rights reserved.
7 //
8 //  This program is free software: you can redistribute it and/or modify
9 //  it under the terms of the GNU General Public License as published by
10 //  the Free Software Foundation, either version 3 of the License, or
11 //  (at your option) any later version.
13 //  This program is distributed in the hope that it will be useful,
14 //  but WITHOUT ANY WARRANTY; without even the implied warranty of
15 //  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 //  GNU General Public License for more details.
18 //  You should have received a copy of the GNU General Public License
19 //  along with this program.  If not, see <http://www.gnu.org/licenses/>.
22 import Foundation
23 import UIKit
25 let BUNDLE_ID = "name.mro.ShaarliOS"
26 let BUNDLE_NAME = "#Shaarli💫"
27 let SELF_URL_PREFIX = BUNDLE_ID
28 let SHAARLI_COMPANION_APP_URL = "https://mro.name/ShaarliOS"
30 func info_to_semver(_ info : [String:Any?]?) -> String {
31     guard let info = info else { return "v?.?" }
32     guard let version = info["CFBundleShortVersionString"] as? String else { return "v?.?" } // Marketing
33     // guard let version = info["CFBundleVersion"] as? String else { return "v?.?" }
34     guard let gitsha = info["CFBundleVersionGitSHA"] as? String else { return "v\(version)" }
35     return "v\(version)+\(gitsha)"
38 // HTTP Basic Auth https://tools.ietf.org/html/rfc7617
40 // Apple URL Loading system is jealous and purges the Authorization header
41 // on iOS 10 and 12 devices. So we have to resort to use a URLSessionTaskDelegate 
42 func httpBasic(_ cre: URLCredential?) -> String? {
43     guard let cre = cre else { return nil }
44     guard cre.user?.count != 0 else { return nil }
45     guard cre.hasPassword else { return nil }
46     // pre-authenticate HTTP Basic Auth https://tools.ietf.org/html/rfc7617
47     // https://gist.github.com/maximbilan/444db1e05babf5b08abae220102fdb8a
48     let uidPwd = "\(cre.user ?? ""):\(cre.password ?? "")"
49     let b64 = uidPwd.data(using:.utf8)!.base64EncodedString()
50     return "Basic \(b64)"
53 // HTTP Basic Auth https://tools.ietf.org/html/rfc7617
55 // empty password is allowed, empty user not.
56 func httpBasic(_ str: String?) -> URLCredential? {
57     // https://gist.github.com/maximbilan/444db1e05babf5b08abae220102fdb8a
58     guard let str = str else { return nil }
59     guard str.hasPrefix("Basic ") else { return nil }
60     let sub = str.suffix(from:.init(utf16Offset:6, in:str))
61     guard let dat = Data(base64Encoded:String(sub)) else { return nil }
62     guard let up = String(data:dat, encoding:.utf8) else { return nil }
63     let arr = up.split(separator:":", maxSplits:1, omittingEmptySubsequences:false)
64     return URLCredential(user:String(arr[0]), password:String(arr[1]), persistence:.forSession)
67 struct ShaarliM {
69     static let buttonColor  = UIColor.init(hue: 87/360.0, saturation: 0.58, brightness: 0.68, alpha:1)
70     static let labelColor   = UIColor.lightText
72     static let shared = ShaarliM()
74     // how can we ever purge these settings? removePersistentDomain
75     let defaults = UserDefaults(suiteName:"group.\(BUNDLE_ID)")!
77     /** https://code.mro.name/mro/ShaarliOS/src/e9009ef466582e806b97d723e5acea885eaa4c7d/ios/ShaarliOS/ShaarliM.m#L133
78      *
79      * # Keychain
80      *
81      * https://l.mro.name/o/p/a8b2sa5/
82      * https://developer.apple.com/library/archive/samplecode/GenericKeychain/Introduction/Intro.html#//apple_ref/doc/uid/DTS40007797
83      * https://developer.apple.com/library/archive/samplecode/GenericKeychain/Listings/GenericKeychain_KeychainPasswordItem_swift.html#//apple_ref/doc/uid/DTS40007797-GenericKeychain_KeychainPasswordItem_swift-DontLinkElementID_7
84      */
85     private func string(forKey:String) -> String? {
86         do {
87             return try KeychainPasswordItem(service:BUNDLE_ID, account:forKey, accessGroup:nil).readPassword()
88         } catch {
89             return nil
90         }
91     }
93     private func set(_ val:String, forKey:String) {
94         do {
95             try KeychainPasswordItem(service:BUNDLE_ID, account:forKey, accessGroup:nil).savePassword(val)
96         } catch {
97             print("ouch")
98         }
99     }
101     private let KEY_title           = "title"
102     private let KEY_auth            = "httpAuth"
103     private let KEY_endpointURL     = "endpointURL"
104     private let KEY_userName        = "userName"
105     private let KEY_passWord        = "passWord"
106     private let KEY_timeout         = "timeout"
107     private let KEY_privateDefault  = "privateDefault"
108     private let KEY_timezone        = "timezone"
109     private let KEY_tagsDefault     = "tagsDefault"
111     func loadEndpointURL() -> URL? {
112         guard let url = string(forKey:KEY_endpointURL)
113             ?? string(forKey:"endpointUrl")
114             else { return nil }
116         guard var uc = URLComponents(string: url)
117             else { return nil }
118         if uc.user == nil || uc.user == "" {
119             uc.user = string(forKey:KEY_userName)
120         }
121         if uc.password == nil || uc.password == ""  {
122             uc.password = string(forKey:KEY_passWord)
123         }
124         return uc.url
125     }
127     func loadBlog(_ prefs :UserDefaults) -> BlogM? {
128         guard let url = loadEndpointURL() else { return nil }
129         let cre = httpBasic(prefs.string(forKey:KEY_auth))
130         let title = prefs.string(forKey:KEY_title)
131             ?? NSLocalizedString("My Shaarli", comment:String(describing:type(of:self)))
132         let to = timeoutFromDouble(prefs.double(forKey:KEY_timeout))
133         let pd = prefs.bool(forKey:KEY_privateDefault)
134         let tizo = TimeZone(identifier:prefs.string(forKey:KEY_timezone) ?? "")
135         let blank = " "
136         let td = (prefs.string(forKey:KEY_tagsDefault) ?? "").trimmingCharacters(in:.whitespacesAndNewlines) + blank
137         return BlogM(endpoint:url, credential:cre, title:title, timeout:to, privateDefault:pd, timezone:tizo, tagsDefault:blank == td
138             ? ""
139             : td)
140     }
142     func saveBlog(_ prefs : UserDefaults, _ blog: BlogM) {
143         let url = blog.endpoint
144         set(url.absoluteString, forKey:KEY_endpointURL) // incl. uid+pwd
145         // redundant, legacy:
146         if let uc = URLComponents(url:url, resolvingAgainstBaseURL:true) {
147           set(uc.user!, forKey:KEY_userName)
148           set(uc.password!, forKey:KEY_passWord)
149         }
150         prefs.set(httpBasic(blog.credential), forKey:KEY_auth)
151         prefs.set(blog.title, forKey:KEY_title)
152         prefs.set(blog.timeout, forKey:KEY_timeout)
153         prefs.set(blog.privateDefault, forKey:KEY_privateDefault)
154         prefs.set(blog.timezone?.identifier, forKey:KEY_timezone)
155         prefs.set(blog.tagsDefault.trimmingCharacters(in:.whitespacesAndNewlines), forKey:KEY_tagsDefault)
156         prefs.removeObject(forKey:"tagsActive")
157     }