5 // Created by Marcus Rohrmoser on 21.02.20.
6 // Copyright © 2019-2022 Marcus Rohrmoser mobile Software http://mro.name/me. All rights reserved.
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/>.
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()
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)
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
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
85 private func string(forKey:String) -> String? {
87 return try KeychainPasswordItem(service:BUNDLE_ID, account:forKey, accessGroup:nil).readPassword()
93 private func set(_ val:String, forKey:String) {
95 try KeychainPasswordItem(service:BUNDLE_ID, account:forKey, accessGroup:nil).savePassword(val)
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")
116 guard var uc = URLComponents(string: url)
118 if uc.user == nil || uc.user == "" {
119 uc.user = string(forKey:KEY_userName)
121 if uc.password == nil || uc.password == "" {
122 uc.password = string(forKey:KEY_passWord)
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) ?? "")
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
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)
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")