Procházet zdrojové kódy

update fix bugs and release for 5.0.60

alqindiirsyam před 3 týdny
rodič
revize
4b479f343f

binární
.DS_Store


+ 4 - 4
AppBuilder/AppBuilder.xcodeproj/project.pbxproj

@@ -568,7 +568,7 @@
 					"$(inherited)",
 					"@executable_path/Frameworks",
 				);
-				MARKETING_VERSION = 5.0.58;
+				MARKETING_VERSION = 5.0.60;
 				PRODUCT_BUNDLE_IDENTIFIER = io.nexilis.appbuilder;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				PROVISIONING_PROFILE_SPECIFIER = "";
@@ -604,7 +604,7 @@
 					"$(inherited)",
 					"@executable_path/Frameworks",
 				);
-				MARKETING_VERSION = 5.0.58;
+				MARKETING_VERSION = 5.0.60;
 				PRODUCT_BUNDLE_IDENTIFIER = io.nexilis.appbuilder;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				PROVISIONING_PROFILE_SPECIFIER = "";
@@ -640,7 +640,7 @@
 					"@executable_path/../../Frameworks",
 				);
 				LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
-				MARKETING_VERSION = 5.0.58;
+				MARKETING_VERSION = 5.0.60;
 				OTHER_CFLAGS = "-fstack-protector-strong";
 				PRODUCT_BUNDLE_IDENTIFIER = io.nexilis.appbuilder.AppBuilderShare;
 				PRODUCT_NAME = "$(TARGET_NAME)";
@@ -679,7 +679,7 @@
 					"@executable_path/../../Frameworks",
 				);
 				LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
-				MARKETING_VERSION = 5.0.58;
+				MARKETING_VERSION = 5.0.60;
 				OTHER_CFLAGS = "-fstack-protector-strong";
 				PRODUCT_BUNDLE_IDENTIFIER = io.nexilis.appbuilder.AppBuilderShare;
 				PRODUCT_NAME = "$(TARGET_NAME)";

+ 74 - 64
AppBuilder/AppBuilderShare/ShareViewController.swift

@@ -10,6 +10,8 @@ import Social
 import UniformTypeIdentifiers
 import AVFoundation
 import QuickLook
+import ImageIO
+import MobileCoreServices
 
 class ShareViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, UISearchBarDelegate, UITextViewDelegate, QLPreviewControllerDataSource {
     let tableView = UITableView()
@@ -193,19 +195,28 @@ class ShareViewController: UIViewController, UITableViewDelegate, UITableViewDat
                 dataShared["idContact"] = selectedContact.id
                 dataShared["data"] = textView.text
                 if typeShareNum == TypeShare.image {
-                    let compressedImageName = "Nexilis_image_\(Int(Date().timeIntervalSince1970 * 1000))_\(selectedImage != nil ? selectedImage.lastPathComponent.components(separatedBy: ".")[0] : "SS_Image").jpeg"
-                    let thumbName = "THUMB_Nexilis_image_\(Int(Date().timeIntervalSince1970 * 1000))_\(selectedImage != nil ? selectedImage.lastPathComponent.components(separatedBy: ".")[0] : "SS_Image").jpeg"
+                    var compressedImageName = "Nexilis_image_\(Int(Date().timeIntervalSince1970 * 1000))_\(selectedImage != nil ? selectedImage.lastPathComponent.components(separatedBy: ".")[0] : "SS_Image").jpeg"
+                    var thumbName = "THUMB_Nexilis_image_\(Int(Date().timeIntervalSince1970 * 1000))_\(selectedImage != nil ? selectedImage.lastPathComponent.components(separatedBy: ".")[0] : "SS_Image").jpeg"
                     if let appGroupURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: nameGroupShare) {
-                        let sharedImageURL = appGroupURL.appendingPathComponent(compressedImageName)
-                        let sharedThumbURL = appGroupURL.appendingPathComponent(thumbName)
-                        if selectedImage != nil {
-                            try? UIImage(contentsOfFile: selectedImage.path)?.jpegData(compressionQuality: 0.25)?.write(to: sharedThumbURL)
-                            if let dataImage = UIImage(contentsOfFile: selectedImage.path)?.jpegData(compressionQuality: 1.0) {
-                                try? dataImage.write(to: sharedImageURL)
+                        var sharedImageURL = appGroupURL.appendingPathComponent(compressedImageName)
+                        var sharedThumbURL = appGroupURL.appendingPathComponent(thumbName)
+                        if let fileURL = selectedImage {
+                            thumbName = "THUMB_Nexilis_image_\(Int(Date().timeIntervalSince1970 * 1000))_\(selectedImage.lastPathComponent)"
+                            sharedThumbURL = appGroupURL.appendingPathComponent(thumbName)
+                            if let thumbImage = downsampleImage(at: fileURL,
+                                                                to: CGSize(width: 300, height: 300),
+                                                                scale: UIScreen.main.scale),
+                               let thumbData = thumbImage.jpegData(compressionQuality: 0.25) {
+                                try? thumbData.write(to: sharedThumbURL)
+                            }
+                            compressedImageName = "Nexilis_image_\(Int(Date().timeIntervalSince1970 * 1000))_\(selectedImage.lastPathComponent)"
+                            sharedImageURL = appGroupURL.appendingPathComponent(compressedImageName)
+                            try? FileManager.default.copyItem(at: fileURL, to: sharedImageURL)
+                        } else if let uiImage = selectedImageTypeImage {
+                            if let thumbData = uiImage.jpegData(compressionQuality: 0.25) {
+                                try? thumbData.write(to: sharedThumbURL)
                             }
-                        } else {
-                            try? selectedImageTypeImage?.jpegData(compressionQuality: 0.25)?.write(to: sharedThumbURL)
-                            if let dataImage = selectedImageTypeImage?.jpegData(compressionQuality: 1.0) {
+                            if let dataImage = uiImage.jpegData(compressionQuality: 1.0) {
                                 try? dataImage.write(to: sharedImageURL)
                             }
                         }
@@ -621,62 +632,40 @@ class ShareViewController: UIViewController, UITableViewDelegate, UITableViewDat
                 }
                 return
             } else if attachment.hasItemConformingToTypeIdentifier(UTType.image.identifier) {
-                // Handle Image
                 attachment.loadItem(forTypeIdentifier: UTType.image.identifier, options: nil) { (imageItem, error) in
-                    if let imageURL = imageItem as? URL {
-                        DispatchQueue.main.async { [self] in
-                            typeShareNum = TypeShare.image
+                    DispatchQueue.main.async { [self] in
+                        typeShareNum = TypeShare.image
+
+                        var imageToShow: UIImage?
+
+                        if let imageURL = imageItem as? URL {
+                            imageToShow = downsampleImage(at: imageURL,
+                                                          to: CGSize(width: view.bounds.width, height: view.bounds.height - 150),
+                                                          scale: UIScreen.main.scale)
                             selectedImage = imageURL
-                            if let viewVc = vcHandleImage.view {
-                                let imageView = UIImageView()
-                                imageView.image = UIImage(contentsOfFile: imageURL.path)
-                                imageView.contentMode = .scaleAspectFit
-                                imageView.clipsToBounds = true
-                                viewVc.addSubview(imageView)
-                                imageView.frame = CGRect(x: 0, y: 70, width: viewVc.bounds.size.width, height: self.view.bounds.height - 150)
-                                
-                                buildAppearance(contact, viewVc)
-                                
-                                vcHandleImage.modalPresentationStyle = .fullScreen
-                                self.navigationController?.present(vcHandleImage, animated: true)
-                            }
-                        }
-                    } else if let data = imageItem as? Data {
-                        DispatchQueue.main.async { [self] in
-                            let image = UIImage(data: data)
-                            typeShareNum = TypeShare.image
-                            selectedImageTypeImage = image
-                            if let viewVc = vcHandleImage.view {
-                                let imageView = UIImageView()
-                                imageView.image = image
-                                imageView.contentMode = .scaleAspectFit
-                                imageView.clipsToBounds = true
-                                viewVc.addSubview(imageView)
-                                imageView.frame = CGRect(x: 0, y: 70, width: viewVc.bounds.size.width, height: self.view.bounds.height - 150)
-                                
-                                buildAppearance(contact, viewVc)
-                                
-                                vcHandleImage.modalPresentationStyle = .fullScreen
-                                self.navigationController?.present(vcHandleImage, animated: true)
-                            }
+                        } else if let data = imageItem as? Data {
+                            let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString + ".tmp")
+                            try? data.write(to: tempURL)
+                            imageToShow = downsampleImage(at: tempURL,
+                                                          to: CGSize(width: view.bounds.width, height: view.bounds.height - 150),
+                                                          scale: UIScreen.main.scale)
+                            selectedImageTypeImage = imageToShow
+                        } else if let uiImage = imageItem as? UIImage {
+                            imageToShow = uiImage
+                            selectedImageTypeImage = uiImage
                         }
-                    } else if let image = imageItem as? UIImage {
-                        DispatchQueue.main.async { [self] in
-                            typeShareNum = TypeShare.image
-                            selectedImageTypeImage = image
-                            if let viewVc = vcHandleImage.view {
-                                let imageView = UIImageView()
-                                imageView.image = image
-                                imageView.contentMode = .scaleAspectFit
-                                imageView.clipsToBounds = true
-                                viewVc.addSubview(imageView)
-                                imageView.frame = CGRect(x: 0, y: 70, width: viewVc.bounds.size.width, height: self.view.bounds.height - 150)
-                                
-                                buildAppearance(contact, viewVc)
-                                
-                                vcHandleImage.modalPresentationStyle = .fullScreen
-                                self.navigationController?.present(vcHandleImage, animated: true)
-                            }
+
+                        if let safeImage = imageToShow, let viewVc = vcHandleImage.view {
+                            let imageView = UIImageView(image: safeImage)
+                            imageView.contentMode = .scaleAspectFit
+                            imageView.clipsToBounds = true
+                            imageView.frame = CGRect(x: 0, y: 70, width: viewVc.bounds.width, height: self.view.bounds.height - 150)
+                            viewVc.addSubview(imageView)
+
+                            buildAppearance(contact, viewVc)
+
+                            vcHandleImage.modalPresentationStyle = .fullScreen
+                            self.navigationController?.present(vcHandleImage, animated: true)
                         }
                     }
                 }
@@ -823,6 +812,27 @@ class ShareViewController: UIViewController, UITableViewDelegate, UITableViewDat
             }
         }
     }
+    
+    func downsampleImage(at imageURL: URL, to pointSize: CGSize, scale: CGFloat) -> UIImage? {
+        let imageSourceOptions = [kCGImageSourceShouldCache: false] as CFDictionary
+        guard let imageSource = CGImageSourceCreateWithURL(imageURL as CFURL, imageSourceOptions) else {
+            return nil
+        }
+
+        let maxDimensionInPixels = max(pointSize.width, pointSize.height) * scale
+        let downsampleOptions = [
+            kCGImageSourceCreateThumbnailFromImageAlways: true,
+            kCGImageSourceShouldCacheImmediately: true,
+            kCGImageSourceCreateThumbnailWithTransform: true,
+            kCGImageSourceThumbnailMaxPixelSize: maxDimensionInPixels
+        ] as CFDictionary
+
+        guard let downsampledImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, downsampleOptions) else {
+            return nil
+        }
+
+        return UIImage(cgImage: downsampledImage)
+    }
 }
 
 struct Contact {

+ 144 - 33
NexilisLite/NexilisLite/Source/APIS.swift

@@ -1426,7 +1426,7 @@ public class APIS: NSObject {
                 let filteredData = jsonArray.filter({
                     let package_id = ($0["package_id"] as! String)
                     if package_id.contains("_fb") {
-                        let listSplit = package_id.split(separator: "_", maxSplits: 2)
+                        let listSplit = package_id.split(separator: "_", maxSplits: 2, omittingEmptySubsequences: false).map { String($0) }
                         let numIdx = listSplit[listSplit.firstIndex(where: { $0.contains("fb") }) ?? 0]
                         let indexTap = Int(String(numIdx).substring(from: 2, to: numIdx.count)) ?? 0
                         return indexTap == idx
@@ -1436,7 +1436,7 @@ public class APIS: NSObject {
                 if filteredData.count != 0 {
                     let data = filteredData[0] as? [String: Any]
                     let package_id = data?["package_id"] as! String
-                    let listSplit = package_id.split(separator: "_", maxSplits: 2)
+                    let listSplit = package_id.split(separator: "_", maxSplits: 2, omittingEmptySubsequences: false).map { String($0) }
                     return String(listSplit[2])
                 }
             }
@@ -2308,6 +2308,22 @@ public class APIS: NSObject {
         }
     }
     
+    static func showMessageGuardFile(mime: String) {
+        alertControllerExpired = LibAlertController(
+            title: "⚠️ Message Guard Announcement".localized(),
+            message: mime == "image/jpeg" ? "Your image have been blocked by Message Guard. Please attach valid image.".localized() : "Your pdf file have been blocked by Message Guard. Please attach valid file.".localized() ,
+            preferredStyle: .alert
+        )
+        
+        alertControllerExpired.addAction(UIAlertAction(title: "OK".localized(), style: .default, handler: nil))
+        
+        if UIApplication.shared.visibleViewController?.navigationController != nil {
+            UIApplication.shared.visibleViewController?.navigationController?.present(alertControllerExpired, animated: true, completion: nil)
+        } else {
+            UIApplication.shared.visibleViewController?.present(alertControllerExpired, animated: true, completion: nil)
+        }
+    }
+    
     private static func showEnableNotificationsAlert() {
         guard !isAlertPresented else { return }
         isAlertPresented = true
@@ -2458,8 +2474,9 @@ public class APIS: NSObject {
                                                 DispatchQueue.main.async {
                                                     Nexilis.hideLoader {
                                                         if result == 1 {
-                                                            copyData()
-                                                            sendIt()
+                                                            if copyData() {
+                                                                sendIt()
+                                                            }
                                                         } else {
                                                             APIS.showWarningFile(type: result)
                                                             resetPrefs()
@@ -2467,26 +2484,70 @@ public class APIS: NSObject {
                                                     }
                                                 }
                                             } else {
-                                                copyData()
-                                                sendIt()
+                                                if copyData() {
+                                                    sendIt()
+                                                }
                                             }
-                                            func copyData() {
-                                                do {
-                                                    let documentDir = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
-                                                    if FileManager.default.fileExists(atPath: sharedImageURL.path) {
-                                                        let file = documentDir.appendingPathComponent(imageId)
-                                                        if !FileManager().fileExists(atPath: file.path) {
-                                                            try? FileManager.default.copyItem(at: sharedImageURL, to: file)
+                                            func copyData() -> Bool {
+                                                var dataSanitized: Data!
+                                                func sanitizeFile(sanitizeAction: (Data) -> MessageGuardLite.Result) -> Data? {
+                                                    DispatchQueue.main.async {
+                                                        Nexilis.showLoader(text: "Sanitizing your image (Message Guard)".localized())
+                                                    }
+                                                    let res = sanitizeAction(try! Data(contentsOf: sharedImageURL))
+                                                    defer {
+                                                        DispatchQueue.main.async { Nexilis.hideLoader {} }
+                                                    }
+
+                                                    if res.verdict == .block {
+                                                        DispatchQueue.main.async {
+                                                            Nexilis.hideLoader {
+                                                                APIS.showMessageGuardFile(mime: res.mime)
+                                                            }
                                                         }
+                                                        return nil
                                                     }
-                                                    if FileManager.default.fileExists(atPath: sharedThumbURL.path) {
-                                                        let file = documentDir.appendingPathComponent(thumb)
-                                                        if !FileManager().fileExists(atPath: file.path) {
-                                                            try? FileManager.default.copyItem(at: sharedThumbURL, to: file)
+                                                    return res.data ?? Data()
+                                                }
+                                                func processIt() {
+                                                    do {
+                                                        let documentDir = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
+                                                        if FileManager.default.fileExists(atPath: sharedImageURL.path) {
+                                                            let file = documentDir.appendingPathComponent(imageId)
+                                                            if !FileManager().fileExists(atPath: file.path) {
+                                                                if dataSanitized != nil {
+                                                                    let file = documentDir.appendingPathComponent(imageId)
+                                                                    try? dataSanitized.write(to: file)
+                                                                } else {
+                                                                    try? FileManager.default.copyItem(at: sharedImageURL, to: file)
+                                                                }
+                                                            }
+                                                        }
+                                                        if FileManager.default.fileExists(atPath: sharedThumbURL.path) {
+                                                            let file = documentDir.appendingPathComponent(thumb)
+                                                            if !FileManager().fileExists(atPath: file.path) {
+                                                                try? FileManager.default.copyItem(at: sharedThumbURL, to: file)
+                                                            }
                                                         }
+                                                    } catch {
+                                                        
                                                     }
-                                                } catch {
-                                                    
+                                                }
+                                                if Nexilis.checkingAccess(key: "message_guard") {
+                                                    DispatchQueue.global().async {
+                                                        let guardLite = MessageGuardLite(limits: .defaults())
+
+                                                        if let sanitized = sanitizeFile(sanitizeAction: guardLite.sanitizeImage) {
+                                                            dataSanitized = sanitized
+                                                        } else { return }
+
+                                                        processIt()
+                                                        sendIt()
+                                                    }
+                                                    return false
+                                                } else {
+                                                    processIt()
+                                                    return true
                                                 }
                                             }
                                         }
@@ -2548,8 +2609,9 @@ public class APIS: NSObject {
                                                 DispatchQueue.main.async {
                                                     Nexilis.hideLoader {
                                                         if result == 1 {
-                                                            copyData()
-                                                            sendIt()
+                                                            if copyData() {
+                                                                sendIt()
+                                                            }
                                                         } else {
                                                             APIS.showWarningFile(type: result)
                                                             resetPrefs()
@@ -2557,21 +2619,70 @@ public class APIS: NSObject {
                                                     }
                                                 }
                                             } else {
-                                                copyData()
-                                                sendIt()
+                                                if copyData() {
+                                                    sendIt()
+                                                }
                                             }
-                                            func copyData() {
-                                                do {
-                                                    let documentDir = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
-                                                    if FileManager.default.fileExists(atPath: sharedFileURL.path) {
-                                                        let file = documentDir.appendingPathComponent(renamedFileId)
-                                                        if !FileManager().fileExists(atPath: file.path) {
-                                                            try? FileManager.default.copyItem(at: sharedFileURL, to: file)
+                                            func copyData() -> Bool {
+                                                var dataSanitized: Data!
+                                                func sanitizeFile(sanitizeAction: (Data) -> MessageGuardLite.Result) -> Data? {
+                                                    DispatchQueue.main.async {
+                                                        Nexilis.showLoader(text: "Sanitizing your pdf file (Message Guard)".localized())
+                                                    }
+                                                    let res = sanitizeAction(try! Data(contentsOf: sharedFileURL))
+                                                    defer {
+                                                        DispatchQueue.main.async { Nexilis.hideLoader {} }
+                                                    }
+
+                                                    if res.verdict == .block {
+                                                        DispatchQueue.main.async {
+                                                            Nexilis.hideLoader {
+                                                                APIS.showMessageGuardFile(mime: res.mime)
+                                                            }
                                                         }
+                                                        return nil
                                                     }
-                                                    data = "\(fileId)|\(data)"
-                                                } catch {
-                                                    
+                                                    return res.data ?? Data()
+                                                }
+                                                
+                                                func processIt() {
+                                                    do {
+                                                        let documentDir = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
+                                                        if FileManager.default.fileExists(atPath: sharedFileURL.path) {
+                                                            let file = documentDir.appendingPathComponent(renamedFileId)
+                                                            if !FileManager().fileExists(atPath: file.path) {
+                                                                if dataSanitized != nil {
+                                                                    let file = documentDir.appendingPathComponent(renamedFileId)
+                                                                    try? dataSanitized.write(to: file)
+                                                                } else {
+                                                                    try? FileManager.default.copyItem(at: sharedFileURL, to: file)
+                                                                }
+                                                            }
+                                                        }
+                                                        data = "\(fileId)|\(data)"
+                                                    } catch {
+                                                        
+                                                    }
+                                                }
+                                                
+                                                if Nexilis.checkingAccess(key: "message_guard") {
+                                                    DispatchQueue.global().async {
+                                                        let guardLite = MessageGuardLite(limits: .defaults())
+                                                        let mimeType = MessageGuardLite.sniffMime(try! Data(contentsOf: sharedFileURL))
+
+                                                        if mimeType == "application/pdf" {
+                                                            if let sanitized = sanitizeFile(sanitizeAction: guardLite.sanitizePdf) {
+                                                                dataSanitized = sanitized
+                                                            } else { return }
+                                                        }
+
+                                                        processIt()
+                                                        sendIt()
+                                                    }
+                                                    return false
+                                                } else {
+                                                    processIt()
+                                                    return true
                                                 }
                                             }
                                         }

+ 3 - 1
NexilisLite/NexilisLite/Source/Extension.swift

@@ -1340,7 +1340,9 @@ extension UIImageView {
 
         // download
 
-        let url = URL(string: urlString)!
+        guard let url = URL(string: urlString) else {
+            return
+        }
         currentURL = url
         let urlConfig = URLSessionConfiguration.default
         let sessionDelegate = SelfSignedURLSessionDelegate()

+ 1 - 1
NexilisLite/NexilisLite/Source/FloatingButton/FloatingButton.swift

@@ -666,7 +666,7 @@ public class FloatingButton: UIView, UIGestureRecognizerDelegate {
         var app_id = sender.accessibilityIdentifier!
         var indexTap = 0
         if package_id.contains("_fb"){
-            let listSplit = package_id.split(separator: "_", maxSplits: 2)
+            let listSplit = package_id.split(separator: "_", maxSplits: 2, omittingEmptySubsequences: false).map { String($0) }
             let numIdx = listSplit[listSplit.firstIndex(where: { $0.contains("fb") }) ?? 0]
             indexTap = Int(String(numIdx).substring(from: 2, to: numIdx.count)) ?? 0
             if listSplit.count == 3 {

+ 1 - 1
NexilisLite/NexilisLite/Source/Nexilis.swift

@@ -19,7 +19,7 @@ import CryptoKit
 import WebKit
 
 public class Nexilis: NSObject {
-    public static var cpaasVersion = "5.0.58"
+    public static var cpaasVersion = "5.0.60"
     public static var sAPIKey = ""
     
     public static var ADDRESS = ""

+ 171 - 0
NexilisLite/NexilisLite/Source/Utils.swift

@@ -14,6 +14,7 @@ import CoreLocation
 import CryptoKit
 import LocalAuthentication
 import AVFoundation
+import PDFKit
 //import var CommonCrypto.CC_MD5_DIGEST_LENGTH
 //import func CommonCrypto.CC_MD5
 //import typealias CommonCrypto.CC_LONG
@@ -3877,3 +3878,173 @@ extension UILabel {
         self.font = UIFont.systemFont(ofSize: 14)
     }
 }
+
+public final class MessageGuardLite {
+    
+    // MARK: - Verdict
+    public enum Verdict {
+        case allow, sanitized, block
+    }
+    
+    // MARK: - Result
+    public struct Result {
+        public let verdict: Verdict
+        public let reason: String
+        public let mime: String
+        public let data: Data?  // nil for some paths (like PDF->images)
+    }
+    
+    // MARK: - Limits
+    public struct Limits {
+        public let maxImagePixels: Int
+        public let maxImageEdge: Int
+        public let pdfMaxPages: Int
+        
+        public init(maxImagePixels: Int, maxImageEdge: Int, pdfMaxPages: Int) {
+            self.maxImagePixels = maxImagePixels
+            self.maxImageEdge = maxImageEdge
+            self.pdfMaxPages = pdfMaxPages
+        }
+        
+        public static func defaults() -> Limits {
+            return Limits(maxImagePixels: 4096 * 4096, maxImageEdge: 4096, pdfMaxPages: 10)
+        }
+    }
+    
+    private let limits: Limits
+    
+    public init(limits: Limits? = nil) {
+        self.limits = limits ?? Limits.defaults()
+    }
+    
+    // MARK: - 1. Text Sanitization
+    public func sanitizeText(_ utf8: Data) -> Result {
+        guard let input = String(data: utf8, encoding: .utf8) else {
+            return Result(verdict: .block,
+                          reason: "Invalid UTF-8 text",
+                          mime: "application/octet-stream",
+                          data: nil)
+        }
+        let pattern = "[\\p{C}\\u200B-\\u200F\\uFEFF\\u202A-\\u202E]"
+        let regex = try! NSRegularExpression(pattern: pattern)
+        let clean = regex.stringByReplacingMatches(in: input,
+                                                   options: [],
+                                                   range: NSRange(location: 0, length: input.utf16.count),
+                                                   withTemplate: "")
+        if input == clean {
+            return Result(verdict: .allow, reason: "No changes", mime: "text/plain", data: utf8)
+        } else {
+            return Result(verdict: .sanitized, reason: "Removed control & zero-width characters", mime: "text/plain", data: clean.data(using: .utf8))
+        }
+    }
+    
+    // MARK: - 2. HTML Sanitization
+    public func sanitizeHtml(_ utf8Html: Data) -> Result {
+        guard let input = String(data: utf8Html, encoding: .utf8) else {
+            return Result(verdict: .block, reason: "Invalid HTML encoding", mime: "application/octet-stream", data: nil)
+        }
+        
+        var clean = input
+        clean = clean.replacingOccurrences(of: "(?is)<(script|style)[^>]*>.*?</\\1>", with: "", options: .regularExpression)
+        clean = clean.replacingOccurrences(of: "\\son\\w+=\"[^\"]*\"", with: "", options: .regularExpression)
+        clean = clean.replacingOccurrences(of: "(?i)javascript:[^\"']*", with: "", options: .regularExpression)
+        
+        if input == clean {
+            return Result(verdict: .allow, reason: "No changes", mime: "text/html", data: utf8Html)
+        } else {
+            return Result(verdict: .sanitized, reason: "Sanitized HTML allowlist", mime: "text/html", data: clean.data(using: .utf8))
+        }
+    }
+    
+    // MARK: - 3. Image Sanitization
+    public func sanitizeImage(_ bytes: Data) -> Result {
+        guard let image = UIImage(data: bytes) else {
+            return Result(verdict: .block, reason: "Unrecognized or corrupt image", mime: "image/jpeg", data: nil)
+        }
+        
+        let pixels = Int(image.size.width * image.size.height)
+        var processed = image
+        
+        if pixels > limits.maxImagePixels {
+            let scale = sqrt(Double(limits.maxImagePixels) / Double(pixels))
+            let newSize = CGSize(width: image.size.width * scale, height: image.size.height * scale)
+            processed = resize(image, to: newSize)
+        }
+        
+        processed = capEdge(processed, maxEdge: limits.maxImageEdge)
+        
+        guard let out = processed.jpegData(compressionQuality: 0.8) else {
+            return Result(verdict: .block, reason: "Failed to re-encode image", mime: "image/jpeg", data: nil)
+        }
+        
+        return Result(verdict: .sanitized,
+                      reason: "Re-encoded PNG (metadata/animation removed)",
+                      mime: "image/png",
+                      data: out)
+    }
+    
+    private func resize(_ image: UIImage, to size: CGSize) -> UIImage {
+        UIGraphicsBeginImageContextWithOptions(size, true, 1.0)
+        image.draw(in: CGRect(origin: .zero, size: size))
+        let newImg = UIGraphicsGetImageFromCurrentImageContext()
+        UIGraphicsEndImageContext()
+        return newImg ?? image
+    }
+    
+    private func capEdge(_ image: UIImage, maxEdge: Int) -> UIImage {
+        let w = image.size.width
+        let h = image.size.height
+        let maxDim = max(w, h)
+        if maxDim <= CGFloat(maxEdge) { return image }
+        
+        let scale = CGFloat(maxEdge) / maxDim
+        let newSize = CGSize(width: w * scale, height: h * scale)
+        return resize(image, to: newSize)
+    }
+    
+    // MARK: - 4. PDF Sanitization
+    public func sanitizePdf(_ pdfData: Data) -> Result {
+        guard let pdf = PDFDocument(data: pdfData) else {
+            return Result(
+                verdict: .block,
+                reason: "Unrecognized or corrupt PDF",
+                mime: "application/octet-stream",
+                data: nil
+            )
+        }
+        
+        // ✅ Allowed as-is
+        return Result(
+            verdict: .allow,
+            reason: "PDF is valid and within limits",
+            mime: "application/pdf",
+            data: pdfData
+        )
+    }
+    
+    // MARK: - 5. MIME Sniffing
+    public static func sniffMime(_ data: Data) -> String {
+        let bytes = [UInt8](data.prefix(8))
+        guard bytes.count >= 4 else { return "application/octet-stream" }
+        
+        if bytes.starts(with: [0x89, 0x50, 0x4E, 0x47]) { return "image/png" }
+        if bytes.starts(with: [0xFF, 0xD8]) { return "image/jpeg" }
+        if bytes.starts(with: [0x47, 0x49, 0x46]) { return "image/gif" }
+        if bytes.starts(with: [0x25, 0x50, 0x44, 0x46]) { return "application/pdf" }
+        if bytes.starts(with: [0x50, 0x4B]) { return "application/zip" }
+        
+        if let s = String(data: data.prefix(32), encoding: .utf8)?
+            .trimmingCharacters(in: .whitespacesAndNewlines).lowercased() {
+            if s.hasPrefix("<!doctype html") || s.hasPrefix("<html") || s.hasPrefix("<body") {
+                return "text/html"
+            }
+        }
+        
+        return "application/octet-stream"
+    }
+    
+    public static func containsHtmlTags(_ input: String) -> Bool {
+        let pattern = ".*<[^>]+>.*"
+        return input.range(of: pattern, options: .regularExpression) != nil
+    }
+}

+ 107 - 17
NexilisLite/NexilisLite/Source/View/Chat/EditorGroup.swift

@@ -1340,9 +1340,11 @@ public class EditorGroup: UIViewController, CLLocationManagerDelegate {
                             self.labelCounter.text = "\(self.counter)"
                         }
                     }
+                } else {
+                    NotificationCenter.default.post(name: NSNotification.Name(rawValue: "reloadTabChats"), object: nil, userInfo: nil)
                 }
             } else {
-//                NotificationCenter.default.post(name: NSNotification.Name(rawValue: "reloadTabChats"), object: nil, userInfo: nil)
+                NotificationCenter.default.post(name: NSNotification.Name(rawValue: "reloadTabChats"), object: nil, userInfo: nil)
             }
         }
     }
@@ -1984,6 +1986,42 @@ public class EditorGroup: UIViewController, CLLocationManagerDelegate {
                 }
             }
         }
+        if Nexilis.checkingAccess(key: "message_guard") {
+            let guardLite = MessageGuardLite(limits: .defaults())
+            var isSanitizedText = false
+            var isSanitizedHtml = false
+            let res = guardLite.sanitizeText(message_text.data(using: .utf8)!)
+            if res.verdict == .sanitized {
+                isSanitizedText = true
+            }
+            if let clean = res.data, let str = String(data: clean, encoding: .utf8) {
+                if MessageGuardLite.containsHtmlTags(str) {
+                    let res2 = guardLite.sanitizeHtml(res.data ?? Data())
+                    if res2.verdict == .sanitized {
+                        isSanitizedHtml = true
+                    }
+                    if let clean2 = res.data, let str2 = String(data: clean, encoding: .utf8) {
+                        message_text = str2
+                    }
+                } else {
+                    message_text = str
+                }
+            }
+            var protectionType = ""
+            if isSanitizedText && isSanitizedHtml {
+                protectionType = "text & html"
+            } else if isSanitizedText {
+                protectionType = "text"
+            } else if isSanitizedHtml {
+                protectionType = "html"
+            }
+            
+            if !protectionType.isEmpty {
+                DispatchQueue.main.async {
+                    self.view.makeToast("Your message is protected with sanitized \(protectionType) (Message Guard)".localized(), duration: 3)
+                }
+            }
+        }
         let message = CoreMessage_TMessageBank.sendMessage(l_pin: dataGroup["group_id"]  as? String ?? "", message_scope_id: message_scope_id, status: status, message_text: message_text, credential: credential, attachment_flag: attachment_flag, ex_blog_id: ex_blog_id, message_large_text: message_large_text, ex_format: ex_format, image_id: image_id, audio_id: audio_id, video_id: video_id, file_id: file_id, thumb_id: thumb_id, reff_id: reff_id, read_receipts: read_receipts, chat_id: dataTopic["chat_id"]  as? String ?? "", is_call_center: is_call_center, call_center_id: call_center_id, opposite_pin: opposite_pin, gif_id: gif_id, isForwarded: "\(is_forwarded)", specFile: specFileString)
         Nexilis.addQueueMessage(message: message)
         let messageId = String(message.mBodies[CoreMessage_TMessageKey.MESSAGE_ID]!)
@@ -2283,10 +2321,14 @@ public class EditorGroup: UIViewController, CLLocationManagerDelegate {
                                 })
                             }
                         } else {
-                            self.sendReadMessageStatus(chat_id: chat_id, f_pin: fPin, message_scope_id: message_scope_id, message_id: message_id)
+                            DispatchQueue.main.sync {
+                                self.sendReadMessageStatus(chat_id: chat_id, f_pin: fPin, message_scope_id: message_scope_id, message_id: message_id)
+                            }
                         }
                     } else {
-                        self.sendReadMessageStatus(chat_id: chat_id, f_pin: fPin, message_scope_id: message_scope_id, message_id: message_id)
+                        DispatchQueue.main.sync {
+                            self.sendReadMessageStatus(chat_id: chat_id, f_pin: fPin, message_scope_id: message_scope_id, message_id: message_id)
+                        }
                     }
                 }
             }
@@ -2822,23 +2864,71 @@ extension EditorGroup: UIDocumentPickerDelegate, DocumentPickerDelegate, QLPrevi
             
             func sendIt() {
                 sender.navigation.dismiss(animated: true, completion: nil)
-                do {
-                    let dataFile = try Data(contentsOf: self.previewItem! as URL)
-                    let urlFile = self.previewItem?.absoluteString
-                    var originaFileName = (urlFile! as NSString).lastPathComponent
-                    originaFileName = NSString(string: originaFileName).removingPercentEncoding!
-                    let renamedNameFile = "Nexilis_\(Date().currentTimeMillis())_" + originaFileName
+
+                guard let previewItem = self.previewItem else { return }
+                guard var dataFile = try? Data(contentsOf: previewItem as URL) else { return }
+
+                func sanitizeFile(mimeType: String, sanitizeAction: (Data) -> MessageGuardLite.Result) -> Data? {
+                    DispatchQueue.main.async {
+                        Nexilis.showLoader(text: "Sanitizing your \(mimeType.contains("pdf") ? "pdf file" : "image") (Message Guard)".localized())
+                    }
+                    let res = sanitizeAction(dataFile)
+                    defer {
+                        DispatchQueue.main.async { Nexilis.hideLoader {} }
+                    }
+
+                    if res.verdict == .block {
+                        DispatchQueue.main.async {
+                            Nexilis.hideLoader {
+                                APIS.showMessageGuardFile(mime: res.mime)
+                            }
+                        }
+                        return nil
+                    }
+                    return res.data ?? Data()
+                }
+
+                func processIt(with data: Data) {
+                    guard let urlFile = self.previewItem?.absoluteString else { return }
+                    let originalFileName = (urlFile as NSString).lastPathComponent.removingPercentEncoding ?? "file"
+                    let renamedNameFile = "Nexilis_\(Date().currentTimeMillis())_\(originalFileName)"
+
                     let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
                     let fileURL = documentsDirectory.appendingPathComponent(renamedNameFile)
+
                     if !FileManager.default.fileExists(atPath: fileURL.path) {
-                        do {
-                            try dataFile.write(to: fileURL)
-                        } catch {
+                        try? data.write(to: fileURL)
+                    }
+
+                    DispatchQueue.main.async {
+                        self.sendChat(
+                            message_text: "\(originalFileName)|",
+                            attachment_flag: "6",
+                            file_id: renamedNameFile,
+                            viewController: self
+                        )
+                    }
+                }
+
+                if Nexilis.checkingAccess(key: "message_guard") {
+                    DispatchQueue.global().async {
+                        let guardLite = MessageGuardLite(limits: .defaults())
+                        let mimeType = MessageGuardLite.sniffMime(dataFile)
+
+                        if mimeType == "image/png" || mimeType == "image/jpeg" {
+                            if let sanitized = sanitizeFile(mimeType: mimeType, sanitizeAction: guardLite.sanitizeImage) {
+                                dataFile = sanitized
+                            } else { return }
+                        } else if mimeType == "application/pdf" {
+                            if let sanitized = sanitizeFile(mimeType: mimeType, sanitizeAction: guardLite.sanitizePdf) {
+                                dataFile = sanitized
+                            } else { return }
                         }
+
+                        processIt(with: dataFile)
                     }
-                    self.sendChat(message_text: "\(originaFileName)|", attachment_flag: "6", file_id: renamedNameFile, viewController: self)
-                } catch {
-                    
+                } else {
+                    processIt(with: dataFile)
                 }
             }
         }
@@ -3210,7 +3300,7 @@ extension EditorGroup: UITextViewDelegate, CustomTextViewPasteDelegate {
                             description = "" // special rule for google
                         } else {
                             description = result.description?.trimmingCharacters(in: .whitespacesAndNewlines)
-                                .nilIfEmpty ?? "No description available"
+                                .nilIfEmpty ?? ""
                         }
                         let imageUrl = self.youtubeThumbnail(from: text)
                             ?? result.image
@@ -6590,7 +6680,7 @@ extension EditorGroup: UITableViewDelegate, UITableViewDataSource, AVAudioPlayer
                             description = "" // special rule for google
                         } else {
                             description = result.description?.trimmingCharacters(in: .whitespacesAndNewlines)
-                                .nilIfEmpty ?? "No description available"
+                                .nilIfEmpty ?? ""
                         }
                         let imageUrl = self.youtubeThumbnail(from: text)
                             ?? result.image

+ 105 - 14
NexilisLite/NexilisLite/Source/View/Chat/EditorPersonal.swift

@@ -1887,6 +1887,8 @@ public class EditorPersonal: UIViewController, ImageVideoPickerDelegate, UIGestu
                             self.labelCounter.text = "\(self.counter)"
                         }
                     }
+                } else {
+                    NotificationCenter.default.post(name: NSNotification.Name(rawValue: "reloadTabChats"), object: nil, userInfo: nil)
                 }
             } else if !self.isContactCenter {
                 NotificationCenter.default.post(name: NSNotification.Name(rawValue: "reloadTabChats"), object: nil, userInfo: nil)
@@ -2824,6 +2826,43 @@ public class EditorPersonal: UIViewController, ImageVideoPickerDelegate, UIGestu
         if isSecret {
             is_secret = 1
         }
+        if Nexilis.checkingAccess(key: "message_guard") {
+            let guardLite = MessageGuardLite(limits: .defaults())
+            var isSanitizedText = false
+            var isSanitizedHtml = false
+            let res = guardLite.sanitizeText(message_text.data(using: .utf8)!)
+            if res.verdict == .sanitized {
+                isSanitizedText = true
+            }
+            if let clean = res.data, let str = String(data: clean, encoding: .utf8) {
+                if MessageGuardLite.containsHtmlTags(str) {
+                    let res2 = guardLite.sanitizeHtml(res.data ?? Data())
+                    if res2.verdict == .sanitized {
+                        isSanitizedHtml = true
+                    }
+                    if let clean2 = res.data, let str2 = String(data: clean, encoding: .utf8) {
+                        message_text = str2
+                    }
+                } else {
+                    message_text = str
+                }
+            }
+            var protectionType = ""
+            if isSanitizedText && isSanitizedHtml {
+                protectionType = "text & html"
+            } else if isSanitizedText {
+                protectionType = "text"
+            } else if isSanitizedHtml {
+                protectionType = "html"
+            }
+            
+            if !protectionType.isEmpty {
+                DispatchQueue.main.async {
+                    self.view.makeToast("Your message is protected with sanitized \(protectionType) (Message Guard)".localized(), duration: 3)
+                }
+            }
+            
+        }
         sendTyping(l_pin: l_pin, isTyping: true)
         let message = CoreMessage_TMessageBank.sendMessage(l_pin: l_pin, message_scope_id: message_scope_id, status: status, message_text: message_text, credential: credential, attachment_flag: attachment_flag, ex_blog_id: ex_blog_id, message_large_text: message_large_text, ex_format: ex_format, image_id: image_id, audio_id: audio_id, video_id: video_id, file_id: file_id, thumb_id: thumb_id, reff_id: reff_id, read_receipts: read_receipts, chat_id: chat_id, is_call_center: is_call_center, call_center_id: call_center_id, opposite_pin: opposite_pin, gif_id: gif_id, isForwarded: "\(is_forwarded)", isSecret: "\(is_secret)", specFile: specFileString)
         Nexilis.addQueueMessage(message: message)
@@ -3447,10 +3486,14 @@ public class EditorPersonal: UIViewController, ImageVideoPickerDelegate, UIGestu
                                 })
                             }
                         } else {
-                            self.sendReadMessageStatus(chat_id: chat_id, f_pin: fPin, message_scope_id: message_scope_id, message_id: message_id)
+                            DispatchQueue.main.sync {
+                                self.sendReadMessageStatus(chat_id: chat_id, f_pin: fPin, message_scope_id: message_scope_id, message_id: message_id)
+                            }
                         }
                     } else {
-                        self.sendReadMessageStatus(chat_id: chat_id, f_pin: fPin, message_scope_id: message_scope_id, message_id: message_id)
+                        DispatchQueue.main.sync {
+                            self.sendReadMessageStatus(chat_id: chat_id, f_pin: fPin, message_scope_id: message_scope_id, message_id: message_id)
+                        }
                     }
                 }
             }
@@ -4143,23 +4186,71 @@ extension EditorPersonal: UIDocumentPickerDelegate, DocumentPickerDelegate, QLPr
             
             func sendIt() {
                 sender.navigation.dismiss(animated: true, completion: nil)
-                do {
-                    let dataFile = try Data(contentsOf: self.previewItem! as URL)
-                    let urlFile = self.previewItem?.absoluteString
-                    var originaFileName = (urlFile! as NSString).lastPathComponent
-                    originaFileName = NSString(string: originaFileName).removingPercentEncoding!
-                    let renamedNameFile = "Nexilis_\(Date().currentTimeMillis())_" + originaFileName
+
+                guard let previewItem = self.previewItem else { return }
+                guard var dataFile = try? Data(contentsOf: previewItem as URL) else { return }
+
+                func sanitizeFile(mimeType: String, sanitizeAction: (Data) -> MessageGuardLite.Result) -> Data? {
+                    DispatchQueue.main.async {
+                        Nexilis.showLoader(text: "Sanitizing your \(mimeType.contains("pdf") ? "pdf file" : "image") (Message Guard)".localized())
+                    }
+                    let res = sanitizeAction(dataFile)
+                    defer {
+                        DispatchQueue.main.async { Nexilis.hideLoader {} }
+                    }
+
+                    if res.verdict == .block {
+                        DispatchQueue.main.async {
+                            Nexilis.hideLoader {
+                                APIS.showMessageGuardFile(mime: res.mime)
+                            }
+                        }
+                        return nil
+                    }
+                    return res.data ?? Data()
+                }
+
+                func processIt(with data: Data) {
+                    guard let urlFile = self.previewItem?.absoluteString else { return }
+                    let originalFileName = (urlFile as NSString).lastPathComponent.removingPercentEncoding ?? "file"
+                    let renamedNameFile = "Nexilis_\(Date().currentTimeMillis())_\(originalFileName)"
+
                     let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
                     let fileURL = documentsDirectory.appendingPathComponent(renamedNameFile)
+
                     if !FileManager.default.fileExists(atPath: fileURL.path) {
-                        do {
-                            try dataFile.write(to: fileURL)
-                        } catch {
+                        try? data.write(to: fileURL)
+                    }
+
+                    DispatchQueue.main.async {
+                        self.sendChat(
+                            message_text: "\(originalFileName)|",
+                            attachment_flag: "6",
+                            file_id: renamedNameFile,
+                            viewController: self
+                        )
+                    }
+                }
+
+                if Nexilis.checkingAccess(key: "message_guard") {
+                    DispatchQueue.global().async {
+                        let guardLite = MessageGuardLite(limits: .defaults())
+                        let mimeType = MessageGuardLite.sniffMime(dataFile)
+
+                        if mimeType == "image/png" || mimeType == "image/jpeg" {
+                            if let sanitized = sanitizeFile(mimeType: mimeType, sanitizeAction: guardLite.sanitizeImage) {
+                                dataFile = sanitized
+                            } else { return }
+                        } else if mimeType == "application/pdf" {
+                            if let sanitized = sanitizeFile(mimeType: mimeType, sanitizeAction: guardLite.sanitizePdf) {
+                                dataFile = sanitized
+                            } else { return }
                         }
+
+                        processIt(with: dataFile)
                     }
-                    self.sendChat(message_text: "\(originaFileName)|", attachment_flag: "6", file_id: renamedNameFile, viewController: self)
-                } catch {
-                    
+                } else {
+                    processIt(with: dataFile)
                 }
             }
         }

+ 1 - 1
NexilisLite/NexilisLite/Source/View/Chat/EditorStarMessages.swift

@@ -885,7 +885,7 @@ public class EditorStarMessages: UIViewController, UITableViewDataSource, UITabl
                             description = "" // special rule for google
                         } else {
                             description = result.description?.trimmingCharacters(in: .whitespacesAndNewlines)
-                                .nilIfEmpty ?? "No description available"
+                                .nilIfEmpty ?? ""
                         }
                         let imageUrl = self.youtubeThumbnail(from: text)
                             ?? result.image

+ 13 - 0
NexilisLite/NexilisLite/Source/View/Chat/PreviewAttachmentImageVideo.swift

@@ -660,6 +660,19 @@ class PreviewAttachmentImageVideo: UIViewController, UIScrollViewDelegate, UITex
                 } else {
                     compressedImage = (imageVideoData![.originalImage] as! UIImage).jpeg ?? Data()
                 }
+                if Nexilis.checkingAccess(key: "message_guard") {
+                    let guardLite = MessageGuardLite(limits: .defaults())
+                    let res = guardLite.sanitizeImage(compressedImage)
+                    if res.verdict != .block {
+                        compressedImage = res.data ?? Data()
+                    } else {
+                        DispatchQueue.main.async {
+                            Nexilis.hideLoader {}
+                        }
+                        APIS.showMessageGuardFile(mime: res.mime)
+                        return
+                    }
+                }
                 if let compressed = compressImageLikeWhatsApp(UIImage(data: compressedImage) ?? UIImage()) {
                     compressedImage = compressed
                 }