NCUtility.swift 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359
  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 false
  47. }
  48. // contentype
  49. for richdocumentMimetype: String in NCGlobal.shared.capabilityRichdocumentsMimetypes {
  50. if richdocumentMimetype.contains(metadata.contentType) || metadata.contentType == "text/plain" {
  51. return true
  52. }
  53. }
  54. // mimetype
  55. if !NCGlobal.shared.capabilityRichdocumentsMimetypes.isEmpty && mimeType.components(separatedBy: ".").count > 2 {
  56. let mimeTypeArray = mimeType.components(separatedBy: ".")
  57. let mimeType = mimeTypeArray[mimeTypeArray.count - 2] + "." + mimeTypeArray[mimeTypeArray.count - 1]
  58. for richdocumentMimetype: String in NCGlobal.shared.capabilityRichdocumentsMimetypes {
  59. if richdocumentMimetype.contains(mimeType) {
  60. return true
  61. }
  62. }
  63. }
  64. return false
  65. }
  66. func isDirectEditing(account: String, contentType: String) -> [String] {
  67. var editor: [String] = []
  68. guard let results = NCManageDatabase.shared.getDirectEditingEditors(account: account) else {
  69. return editor
  70. }
  71. for result: tableDirectEditingEditors in results {
  72. for mimetype in result.mimetypes {
  73. if mimetype == contentType {
  74. editor.append(result.editor)
  75. }
  76. // HARDCODE
  77. // https://github.com/nextcloud/text/issues/913
  78. if mimetype == "text/markdown" && contentType == "text/x-markdown" {
  79. editor.append(result.editor)
  80. }
  81. if contentType == "text/html" {
  82. editor.append(result.editor)
  83. }
  84. }
  85. for mimetype in result.optionalMimetypes {
  86. if mimetype == contentType {
  87. editor.append(result.editor)
  88. }
  89. }
  90. }
  91. return Array(Set(editor))
  92. }
  93. func permissionsContainsString(_ metadataPermissions: String, permissions: String) -> Bool {
  94. for char in permissions {
  95. if metadataPermissions.contains(char) == false {
  96. return false
  97. }
  98. }
  99. return true
  100. }
  101. func getCustomUserAgentNCText() -> String {
  102. if UIDevice.current.userInterfaceIdiom == .phone {
  103. // NOTE: Hardcoded (May 2022)
  104. // Tested for iPhone SE (1st), iOS 12 iPhone Pro Max, iOS 15.4
  105. // 605.1.15 = WebKit build version
  106. // 15E148 = frozen iOS build number according to: https://chromestatus.com/feature/4558585463832576
  107. return userAgent + " " + "AppleWebKit/605.1.15 Mobile/15E148"
  108. } else {
  109. return userAgent
  110. }
  111. }
  112. func getCustomUserAgentOnlyOffice() -> String {
  113. let appVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString")!
  114. if UIDevice.current.userInterfaceIdiom == .pad {
  115. return "Mozilla/5.0 (iPad) Nextcloud-iOS/\(appVersion)"
  116. } else {
  117. return "Mozilla/5.0 (iPhone) Mobile Nextcloud-iOS/\(appVersion)"
  118. }
  119. }
  120. @objc func isQuickLookDisplayable(metadata: tableMetadata) -> Bool {
  121. return true
  122. }
  123. @objc func ocIdToFileId(ocId: String?) -> String? {
  124. guard let ocId = ocId else { return nil }
  125. let items = ocId.components(separatedBy: "oc")
  126. if items.count < 2 { return nil }
  127. guard let intFileId = Int(items[0]) else { return nil }
  128. return String(intFileId)
  129. }
  130. func getUserStatus(userIcon: String?, userStatus: String?, userMessage: String?) -> (onlineStatus: UIImage?, statusMessage: String, descriptionMessage: String) {
  131. var onlineStatus: UIImage?
  132. var statusMessage: String = ""
  133. var descriptionMessage: String = ""
  134. var messageUserDefined: String = ""
  135. if userStatus?.lowercased() == "online" {
  136. 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)
  137. messageUserDefined = NSLocalizedString("_online_", comment: "")
  138. }
  139. if userStatus?.lowercased() == "away" {
  140. 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)
  141. messageUserDefined = NSLocalizedString("_away_", comment: "")
  142. }
  143. if userStatus?.lowercased() == "dnd" {
  144. onlineStatus = UIImage(named: "userStatusDnd")?.resizeImage(size: CGSize(width: 100, height: 100), isAspectRation: false)
  145. messageUserDefined = NSLocalizedString("_dnd_", comment: "")
  146. descriptionMessage = NSLocalizedString("_dnd_description_", comment: "")
  147. }
  148. if userStatus?.lowercased() == "offline" || userStatus?.lowercased() == "invisible" {
  149. onlineStatus = UIImage(named: "userStatusOffline")!.image(color: .black, size: 50)
  150. messageUserDefined = NSLocalizedString("_invisible_", comment: "")
  151. descriptionMessage = NSLocalizedString("_invisible_description_", comment: "")
  152. }
  153. if let userIcon = userIcon {
  154. statusMessage = userIcon + " "
  155. }
  156. if let userMessage = userMessage {
  157. statusMessage += userMessage
  158. }
  159. statusMessage = statusMessage.trimmingCharacters(in: .whitespaces)
  160. if statusMessage.isEmpty {
  161. statusMessage = messageUserDefined
  162. }
  163. return(onlineStatus, statusMessage, descriptionMessage)
  164. }
  165. @objc func getVersionApp(withBuild: Bool = true) -> String {
  166. if let dictionary = Bundle.main.infoDictionary {
  167. if let version = dictionary["CFBundleShortVersionString"], let build = dictionary["CFBundleVersion"] {
  168. if withBuild {
  169. return "\(version).\(build)"
  170. } else {
  171. return "\(version)"
  172. }
  173. }
  174. }
  175. return ""
  176. }
  177. /*
  178. Facebook's comparison algorithm:
  179. */
  180. func compare(tolerance: Float, expected: Data, observed: Data) throws -> Bool {
  181. enum customError: Error {
  182. case unableToGetUIImageFromData
  183. case unableToGetCGImageFromData
  184. case unableToGetColorSpaceFromCGImage
  185. case imagesHasDifferentSizes
  186. case unableToInitializeContext
  187. }
  188. guard let expectedUIImage = UIImage(data: expected), let observedUIImage = UIImage(data: observed) else {
  189. throw customError.unableToGetUIImageFromData
  190. }
  191. guard let expectedCGImage = expectedUIImage.cgImage, let observedCGImage = observedUIImage.cgImage else {
  192. throw customError.unableToGetCGImageFromData
  193. }
  194. guard let expectedColorSpace = expectedCGImage.colorSpace, let observedColorSpace = observedCGImage.colorSpace else {
  195. throw customError.unableToGetColorSpaceFromCGImage
  196. }
  197. if expectedCGImage.width != observedCGImage.width || expectedCGImage.height != observedCGImage.height {
  198. throw customError.imagesHasDifferentSizes
  199. }
  200. let imageSize = CGSize(width: expectedCGImage.width, height: expectedCGImage.height)
  201. let numberOfPixels = Int(imageSize.width * imageSize.height)
  202. // Checking that our `UInt32` buffer has same number of bytes as image has.
  203. let bytesPerRow = min(expectedCGImage.bytesPerRow, observedCGImage.bytesPerRow)
  204. assert(MemoryLayout<UInt32>.stride == bytesPerRow / Int(imageSize.width))
  205. let expectedPixels = UnsafeMutablePointer<UInt32>.allocate(capacity: numberOfPixels)
  206. let observedPixels = UnsafeMutablePointer<UInt32>.allocate(capacity: numberOfPixels)
  207. let expectedPixelsRaw = UnsafeMutableRawPointer(expectedPixels)
  208. let observedPixelsRaw = UnsafeMutableRawPointer(observedPixels)
  209. let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue)
  210. guard let expectedContext = CGContext(data: expectedPixelsRaw, width: Int(imageSize.width), height: Int(imageSize.height),
  211. bitsPerComponent: expectedCGImage.bitsPerComponent, bytesPerRow: bytesPerRow,
  212. space: expectedColorSpace, bitmapInfo: bitmapInfo.rawValue) else {
  213. expectedPixels.deallocate()
  214. observedPixels.deallocate()
  215. throw customError.unableToInitializeContext
  216. }
  217. guard let observedContext = CGContext(data: observedPixelsRaw, width: Int(imageSize.width), height: Int(imageSize.height),
  218. bitsPerComponent: observedCGImage.bitsPerComponent, bytesPerRow: bytesPerRow,
  219. space: observedColorSpace, bitmapInfo: bitmapInfo.rawValue) else {
  220. expectedPixels.deallocate()
  221. observedPixels.deallocate()
  222. throw customError.unableToInitializeContext
  223. }
  224. expectedContext.draw(expectedCGImage, in: CGRect(origin: .zero, size: imageSize))
  225. observedContext.draw(observedCGImage, in: CGRect(origin: .zero, size: imageSize))
  226. let expectedBuffer = UnsafeBufferPointer(start: expectedPixels, count: numberOfPixels)
  227. let observedBuffer = UnsafeBufferPointer(start: observedPixels, count: numberOfPixels)
  228. var isEqual = true
  229. if tolerance == 0 {
  230. isEqual = expectedBuffer.elementsEqual(observedBuffer)
  231. } else {
  232. // Go through each pixel in turn and see if it is different
  233. var numDiffPixels = 0
  234. for pixel in 0 ..< numberOfPixels where expectedBuffer[pixel] != observedBuffer[pixel] {
  235. // If this pixel is different, increment the pixel diff count and see if we have hit our limit.
  236. numDiffPixels += 1
  237. let percentage = 100 * Float(numDiffPixels) / Float(numberOfPixels)
  238. if percentage > tolerance {
  239. isEqual = false
  240. break
  241. }
  242. }
  243. }
  244. expectedPixels.deallocate()
  245. observedPixels.deallocate()
  246. return isEqual
  247. }
  248. func getLocation(latitude: Double, longitude: Double, completion: @escaping (String?) -> Void) {
  249. let geocoder = CLGeocoder()
  250. let llocation = CLLocation(latitude: latitude, longitude: longitude)
  251. if let location = NCManageDatabase.shared.getLocationFromLatAndLong(latitude: latitude, longitude: longitude) {
  252. completion(location)
  253. } else {
  254. geocoder.reverseGeocodeLocation(llocation) { placemarks, error in
  255. if error == nil, let placemark = placemarks?.first {
  256. let locationComponents: [String] = [placemark.name, placemark.locality, placemark.country]
  257. .compactMap {$0}
  258. let location = locationComponents.joined(separator: ", ")
  259. NCManageDatabase.shared.addGeocoderLocation(location, latitude: latitude, longitude: longitude)
  260. completion(location)
  261. }
  262. }
  263. }
  264. }
  265. // https://stackoverflow.com/questions/5887248/ios-app-maximum-memory-budget/19692719#19692719
  266. // https://stackoverflow.com/questions/27556807/swift-pointer-problems-with-mach-task-basic-info/27559770#27559770
  267. func getMemoryUsedAndDeviceTotalInMegabytes() -> (Float, Float) {
  268. var usedmegabytes: Float = 0
  269. let totalbytes = Float(ProcessInfo.processInfo.physicalMemory)
  270. let totalmegabytes = totalbytes / 1024.0 / 1024.0
  271. var info = mach_task_basic_info()
  272. var count = mach_msg_type_number_t(MemoryLayout<mach_task_basic_info>.size) / 4
  273. let kerr: kern_return_t = withUnsafeMutablePointer(to: &info) {
  274. $0.withMemoryRebound(to: integer_t.self, capacity: 1) {
  275. task_info(
  276. mach_task_self_,
  277. task_flavor_t(MACH_TASK_BASIC_INFO),
  278. $0,
  279. &count
  280. )
  281. }
  282. }
  283. if kerr == KERN_SUCCESS {
  284. let usedbytes: Float = Float(info.resident_size)
  285. usedmegabytes = usedbytes / 1024.0 / 1024.0
  286. }
  287. return (usedmegabytes, totalmegabytes)
  288. }
  289. func removeForbiddenCharacters(_ fileName: String) -> String {
  290. var fileName = fileName
  291. let arrayForbiddenCharacters = ["/"]
  292. for character in arrayForbiddenCharacters {
  293. fileName = fileName.replacingOccurrences(of: character, with: "")
  294. }
  295. return fileName
  296. }
  297. }