NCUtility.swift 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
  1. //
  2. // NCUtility.swift
  3. // Nextcloud
  4. //
  5. // Created by Marino Faggiana on 25/06/18.
  6. // Copyright © 2018 Marino Faggiana. All rights reserved.
  7. //
  8. // Author Marino Faggiana <marino.faggiana@nextcloud.com>
  9. //
  10. // This program is free software: you can redistribute it and/or modify
  11. // it under the terms of the GNU General Public License as published by
  12. // the Free Software Foundation, either version 3 of the License, or
  13. // (at your option) any later version.
  14. //
  15. // This program is distributed in the hope that it will be useful,
  16. // but WITHOUT ANY WARRANTY without even the implied warranty of
  17. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  18. // GNU General Public License for more details.
  19. //
  20. // You should have received a copy of the GNU General Public License
  21. // along with this program. If not, see <http://www.gnu.org/licenses/>.
  22. //
  23. import UIKit
  24. import NextcloudKit
  25. import PDFKit
  26. import Accelerate
  27. import CoreMedia
  28. import Photos
  29. import Alamofire
  30. class NCUtility: NSObject {
  31. let utilityFileSystem = NCUtilityFileSystem()
  32. @objc func isSimulatorOrTestFlight() -> Bool {
  33. guard let path = Bundle.main.appStoreReceiptURL?.path else {
  34. return false
  35. }
  36. return path.contains("CoreSimulator") || path.contains("sandboxReceipt")
  37. }
  38. func isSimulator() -> Bool {
  39. guard let path = Bundle.main.appStoreReceiptURL?.path else {
  40. return false
  41. }
  42. return path.contains("CoreSimulator")
  43. }
  44. func isRichDocument(_ metadata: tableMetadata) -> Bool {
  45. guard let mimeType = CCUtility.getMimeType(metadata.fileNameView) else {
  46. return true
  47. }
  48. // contentype
  49. if !NCGlobal.shared.capabilityRichdocumentsMimetypes.filter({ $0.contains(metadata.contentType) || $0.contains("text/plain") }).isEmpty {
  50. return true
  51. }
  52. // mimetype
  53. if !NCGlobal.shared.capabilityRichdocumentsMimetypes.isEmpty && mimeType.components(separatedBy: ".").count > 2 {
  54. let mimeTypeArray = mimeType.components(separatedBy: ".")
  55. let mimeType = mimeTypeArray[mimeTypeArray.count - 2] + "." + mimeTypeArray[mimeTypeArray.count - 1]
  56. if !NCGlobal.shared.capabilityRichdocumentsMimetypes.filter({ $0.contains(mimeType) }).isEmpty {
  57. return true
  58. }
  59. }
  60. return false
  61. }
  62. func isDirectEditing(account: String, contentType: String) -> [String] {
  63. var editor: [String] = []
  64. guard let results = NCManageDatabase.shared.getDirectEditingEditors(account: account) else {
  65. return editor
  66. }
  67. for result: tableDirectEditingEditors in results {
  68. for mimetype in result.mimetypes {
  69. if mimetype == contentType {
  70. editor.append(result.editor)
  71. }
  72. // HARDCODE
  73. // https://github.com/nextcloud/text/issues/913
  74. if mimetype == "text/markdown" && contentType == "text/x-markdown" {
  75. editor.append(result.editor)
  76. }
  77. if contentType == "text/html" {
  78. editor.append(result.editor)
  79. }
  80. }
  81. for mimetype in result.optionalMimetypes {
  82. if mimetype == contentType {
  83. editor.append(result.editor)
  84. }
  85. }
  86. }
  87. return Array(Set(editor))
  88. }
  89. func permissionsContainsString(_ metadataPermissions: String, permissions: String) -> Bool {
  90. for char in permissions {
  91. if metadataPermissions.contains(char) == false {
  92. return false
  93. }
  94. }
  95. return true
  96. }
  97. func getCustomUserAgentNCText() -> String {
  98. if UIDevice.current.userInterfaceIdiom == .phone {
  99. // NOTE: Hardcoded (May 2022)
  100. // Tested for iPhone SE (1st), iOS 12 iPhone Pro Max, iOS 15.4
  101. // 605.1.15 = WebKit build version
  102. // 15E148 = frozen iOS build number according to: https://chromestatus.com/feature/4558585463832576
  103. return userAgent + " " + "AppleWebKit/605.1.15 Mobile/15E148"
  104. } else {
  105. return userAgent
  106. }
  107. }
  108. func getCustomUserAgentOnlyOffice() -> String {
  109. let appVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString")!
  110. if UIDevice.current.userInterfaceIdiom == .pad {
  111. return "Mozilla/5.0 (iPad) Nextcloud-iOS/\(appVersion)"
  112. } else {
  113. return "Mozilla/5.0 (iPhone) Mobile Nextcloud-iOS/\(appVersion)"
  114. }
  115. }
  116. @objc func isQuickLookDisplayable(metadata: tableMetadata) -> Bool {
  117. return true
  118. }
  119. @objc func ocIdToFileId(ocId: String?) -> String? {
  120. guard let ocId = ocId else { return nil }
  121. let items = ocId.components(separatedBy: "oc")
  122. if items.count < 2 { return nil }
  123. guard let intFileId = Int(items[0]) else { return nil }
  124. return String(intFileId)
  125. }
  126. func getUserStatus(userIcon: String?, userStatus: String?, userMessage: String?) -> (onlineStatus: UIImage?, statusMessage: String, descriptionMessage: String) {
  127. var onlineStatus: UIImage?
  128. var statusMessage: String = ""
  129. var descriptionMessage: String = ""
  130. var messageUserDefined: String = ""
  131. if userStatus?.lowercased() == "online" {
  132. onlineStatus = UIImage(named: "circle_fill")!.image(color: UIColor(red: 103.0 / 255.0, green: 176.0 / 255.0, blue: 134.0 / 255.0, alpha: 1.0), size: 50)
  133. messageUserDefined = NSLocalizedString("_online_", comment: "")
  134. }
  135. if userStatus?.lowercased() == "away" {
  136. onlineStatus = UIImage(named: "userStatusAway")!.image(color: UIColor(red: 233.0 / 255.0, green: 166.0 / 255.0, blue: 75.0 / 255.0, alpha: 1.0), size: 50)
  137. messageUserDefined = NSLocalizedString("_away_", comment: "")
  138. }
  139. if userStatus?.lowercased() == "dnd" {
  140. onlineStatus = UIImage(named: "userStatusDnd")?.resizeImage(size: CGSize(width: 100, height: 100), isAspectRation: false)
  141. messageUserDefined = NSLocalizedString("_dnd_", comment: "")
  142. descriptionMessage = NSLocalizedString("_dnd_description_", comment: "")
  143. }
  144. if userStatus?.lowercased() == "offline" || userStatus?.lowercased() == "invisible" {
  145. onlineStatus = UIImage(named: "userStatusOffline")!.withTintColor(.init(named: "SystemBackgroundInverted")!)
  146. messageUserDefined = NSLocalizedString("_invisible_", comment: "")
  147. descriptionMessage = NSLocalizedString("_invisible_description_", comment: "")
  148. }
  149. if let userIcon = userIcon {
  150. statusMessage = userIcon + " "
  151. }
  152. if let userMessage = userMessage {
  153. statusMessage += userMessage
  154. }
  155. statusMessage = statusMessage.trimmingCharacters(in: .whitespaces)
  156. if statusMessage.isEmpty {
  157. statusMessage = messageUserDefined
  158. }
  159. return(onlineStatus, statusMessage, descriptionMessage)
  160. }
  161. @objc func getVersionApp(withBuild: Bool = true) -> String {
  162. if let dictionary = Bundle.main.infoDictionary {
  163. if let version = dictionary["CFBundleShortVersionString"], let build = dictionary["CFBundleVersion"] {
  164. if withBuild {
  165. return "\(version).\(build)"
  166. } else {
  167. return "\(version)"
  168. }
  169. }
  170. }
  171. return ""
  172. }
  173. /*
  174. Facebook's comparison algorithm:
  175. */
  176. func compare(tolerance: Float, expected: Data, observed: Data) throws -> Bool {
  177. enum customError: Error {
  178. case unableToGetUIImageFromData
  179. case unableToGetCGImageFromData
  180. case unableToGetColorSpaceFromCGImage
  181. case imagesHasDifferentSizes
  182. case unableToInitializeContext
  183. }
  184. guard let expectedUIImage = UIImage(data: expected), let observedUIImage = UIImage(data: observed) else {
  185. throw customError.unableToGetUIImageFromData
  186. }
  187. guard let expectedCGImage = expectedUIImage.cgImage, let observedCGImage = observedUIImage.cgImage else {
  188. throw customError.unableToGetCGImageFromData
  189. }
  190. guard let expectedColorSpace = expectedCGImage.colorSpace, let observedColorSpace = observedCGImage.colorSpace else {
  191. throw customError.unableToGetColorSpaceFromCGImage
  192. }
  193. if expectedCGImage.width != observedCGImage.width || expectedCGImage.height != observedCGImage.height {
  194. throw customError.imagesHasDifferentSizes
  195. }
  196. let imageSize = CGSize(width: expectedCGImage.width, height: expectedCGImage.height)
  197. let numberOfPixels = Int(imageSize.width * imageSize.height)
  198. // Checking that our `UInt32` buffer has same number of bytes as image has.
  199. let bytesPerRow = min(expectedCGImage.bytesPerRow, observedCGImage.bytesPerRow)
  200. assert(MemoryLayout<UInt32>.stride == bytesPerRow / Int(imageSize.width))
  201. let expectedPixels = UnsafeMutablePointer<UInt32>.allocate(capacity: numberOfPixels)
  202. let observedPixels = UnsafeMutablePointer<UInt32>.allocate(capacity: numberOfPixels)
  203. let expectedPixelsRaw = UnsafeMutableRawPointer(expectedPixels)
  204. let observedPixelsRaw = UnsafeMutableRawPointer(observedPixels)
  205. let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue)
  206. guard let expectedContext = CGContext(data: expectedPixelsRaw, width: Int(imageSize.width), height: Int(imageSize.height),
  207. bitsPerComponent: expectedCGImage.bitsPerComponent, bytesPerRow: bytesPerRow,
  208. space: expectedColorSpace, bitmapInfo: bitmapInfo.rawValue) else {
  209. expectedPixels.deallocate()
  210. observedPixels.deallocate()
  211. throw customError.unableToInitializeContext
  212. }
  213. guard let observedContext = CGContext(data: observedPixelsRaw, width: Int(imageSize.width), height: Int(imageSize.height),
  214. bitsPerComponent: observedCGImage.bitsPerComponent, bytesPerRow: bytesPerRow,
  215. space: observedColorSpace, bitmapInfo: bitmapInfo.rawValue) else {
  216. expectedPixels.deallocate()
  217. observedPixels.deallocate()
  218. throw customError.unableToInitializeContext
  219. }
  220. expectedContext.draw(expectedCGImage, in: CGRect(origin: .zero, size: imageSize))
  221. observedContext.draw(observedCGImage, in: CGRect(origin: .zero, size: imageSize))
  222. let expectedBuffer = UnsafeBufferPointer(start: expectedPixels, count: numberOfPixels)
  223. let observedBuffer = UnsafeBufferPointer(start: observedPixels, count: numberOfPixels)
  224. var isEqual = true
  225. if tolerance == 0 {
  226. isEqual = expectedBuffer.elementsEqual(observedBuffer)
  227. } else {
  228. // Go through each pixel in turn and see if it is different
  229. var numDiffPixels = 0
  230. for pixel in 0 ..< numberOfPixels where expectedBuffer[pixel] != observedBuffer[pixel] {
  231. // If this pixel is different, increment the pixel diff count and see if we have hit our limit.
  232. numDiffPixels += 1
  233. let percentage = 100 * Float(numDiffPixels) / Float(numberOfPixels)
  234. if percentage > tolerance {
  235. isEqual = false
  236. break
  237. }
  238. }
  239. }
  240. expectedPixels.deallocate()
  241. observedPixels.deallocate()
  242. return isEqual
  243. }
  244. func getLocation(latitude: Double, longitude: Double, completion: @escaping (String?) -> Void) {
  245. let geocoder = CLGeocoder()
  246. let llocation = CLLocation(latitude: latitude, longitude: longitude)
  247. if let location = NCManageDatabase.shared.getLocationFromLatAndLong(latitude: latitude, longitude: longitude) {
  248. completion(location)
  249. } else {
  250. geocoder.reverseGeocodeLocation(llocation) { placemarks, error in
  251. if error == nil, let placemark = placemarks?.first {
  252. let locationComponents: [String] = [placemark.name, placemark.locality, placemark.country]
  253. .compactMap {$0}
  254. let location = locationComponents.joined(separator: ", ")
  255. NCManageDatabase.shared.addGeocoderLocation(location, latitude: latitude, longitude: longitude)
  256. completion(location)
  257. }
  258. }
  259. }
  260. }
  261. // https://stackoverflow.com/questions/5887248/ios-app-maximum-memory-budget/19692719#19692719
  262. // https://stackoverflow.com/questions/27556807/swift-pointer-problems-with-mach-task-basic-info/27559770#27559770
  263. func getMemoryUsedAndDeviceTotalInMegabytes() -> (Float, Float) {
  264. var usedmegabytes: Float = 0
  265. let totalbytes = Float(ProcessInfo.processInfo.physicalMemory)
  266. let totalmegabytes = totalbytes / 1024.0 / 1024.0
  267. var info = mach_task_basic_info()
  268. var count = mach_msg_type_number_t(MemoryLayout<mach_task_basic_info>.size) / 4
  269. let kerr: kern_return_t = withUnsafeMutablePointer(to: &info) {
  270. $0.withMemoryRebound(to: integer_t.self, capacity: 1) {
  271. task_info(
  272. mach_task_self_,
  273. task_flavor_t(MACH_TASK_BASIC_INFO),
  274. $0,
  275. &count
  276. )
  277. }
  278. }
  279. if kerr == KERN_SUCCESS {
  280. let usedbytes: Float = Float(info.resident_size)
  281. usedmegabytes = usedbytes / 1024.0 / 1024.0
  282. }
  283. return (usedmegabytes, totalmegabytes)
  284. }
  285. func removeForbiddenCharacters(_ fileName: String) -> String {
  286. var fileName = fileName
  287. for character in NCGlobal.shared.forbiddenCharacters {
  288. fileName = fileName.replacingOccurrences(of: character, with: "")
  289. }
  290. return fileName
  291. }
  292. }