Dactyloidae/mobile/ios/Client/Frontend/Share/ShareExtensionHelper.swift
2026-06-26 21:04:09 -07:00

148 lines
6.3 KiB
Swift

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import Foundation
import Shared
import OnePasswordExtension
private let log = Logger.browserLogger
class ShareExtensionHelper: NSObject {
fileprivate weak var selectedTab: Tab?
fileprivate let selectedURL: URL
fileprivate var onePasswordExtensionItem: NSExtensionItem!
fileprivate let browserFillIdentifier = "org.appextension.fill-browser-action"
init(url: URL, tab: Tab?) {
self.selectedURL = tab?.canonicalURL?.displayURL ?? url
self.selectedTab = tab
}
func createActivityViewController(_ completionHandler: @escaping (_ completed: Bool, _ activityType: String?) -> Void) -> UIActivityViewController {
var activityItems = [AnyObject]()
let printInfo = UIPrintInfo(dictionary: nil)
let absoluteString = selectedTab?.url?.absoluteString ?? selectedURL.absoluteString
printInfo.jobName = absoluteString
printInfo.outputType = .general
activityItems.append(printInfo)
if let tab = selectedTab {
activityItems.append(TabPrintPageRenderer(tab: tab))
}
if let title = selectedTab?.title {
activityItems.append(TitleActivityItemProvider(title: title))
}
activityItems.append(self)
let activityViewController = UIActivityViewController(activityItems: activityItems, applicationActivities: nil)
// Hide 'Add to Reading List' which currently uses Safari.
// We would also hide View Later, if possible, but the exclusion list doesn't currently support
// third-party activity types (rdar://19430419).
activityViewController.excludedActivityTypes = [
UIActivityType.addToReadingList,
]
// This needs to be ready by the time the share menu has been displayed and
// activityViewController(activityViewController:, activityType:) is called,
// which is after the user taps the button. So a million cycles away.
findLoginExtensionItem()
activityViewController.completionWithItemsHandler = { activityType, completed, returnedItems, activityError in
if !completed {
completionHandler(completed, activityType.map { $0.rawValue })
return
}
// Bug 1392418 - When copying a url using the share extension there are 2 urls in the pasteboard.
// Make sure the pasteboard only has one url.
if let url = UIPasteboard.general.urls?.first {
UIPasteboard.general.urls = [url]
}
if self.isPasswordManagerActivityType(activityType.map { $0.rawValue }) {
if let logins = returnedItems {
self.fillPasswords(logins as [AnyObject])
}
}
completionHandler(completed, activityType.map { $0.rawValue })
}
return activityViewController
}
}
extension ShareExtensionHelper: UIActivityItemSource {
func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any {
return selectedURL
}
// IMPORTANT: This method needs Swift compiler optimization DISABLED to prevent a nasty
// crash from happening in release builds. It seems as though the check for `nil` may
// get removed by the optimizer which leads to a crash when that happens.
@_semantics("optimize.sil.never") func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivityType) -> Any? {
// activityType actually is nil sometimes (in the simulator at least)
if activityType != nil && isPasswordManagerActivityType(activityType.rawValue) {
return onePasswordExtensionItem
} else {
// Return the URL for the selected tab. If we are in reader view then decode
// it so that we copy the original and not the internal localhost one.
return selectedURL.isReaderModeURL ? selectedURL.decodeReaderModeURL : selectedURL
}
}
func activityViewController(_ activityViewController: UIActivityViewController, dataTypeIdentifierForActivityType activityType: UIActivityType?) -> String {
if let type = activityType, isPasswordManagerActivityType(type.rawValue) {
return browserFillIdentifier
}
return activityType == nil ? browserFillIdentifier : kUTTypeURL as String
}
}
private extension ShareExtensionHelper {
func isPasswordManagerActivityType(_ activityType: String?) -> Bool {
// A 'password' substring covers the most cases, such as pwsafe and 1Password.
// com.agilebits.onepassword-ios.extension
// com.app77.ios.pwsafe2.find-login-action-password-actionExtension
// If your extension's bundle identifier does not contain "password", simply submit a pull request by adding your bundle identifier.
return (activityType?.range(of: "password") != nil)
|| (activityType == "com.lastpass.ilastpass.LastPassExt")
|| (activityType == "in.sinew.Walletx.WalletxExt")
|| (activityType == "com.8bit.bitwarden.find-login-action-extension")
}
func findLoginExtensionItem() {
guard let selectedWebView = selectedTab?.webView else {
return
}
// Add 1Password to share sheet
OnePasswordExtension.shared().createExtensionItem(forWebView: selectedWebView, completion: {(extensionItem, error) -> Void in
if extensionItem == nil {
log.error("Failed to create the password manager extension item: \(error.debugDescription).")
return
}
// Set the 1Password extension item property
self.onePasswordExtensionItem = extensionItem
})
}
func fillPasswords(_ returnedItems: [AnyObject]) {
guard let selectedWebView = selectedTab?.webView else {
return
}
OnePasswordExtension.shared().fillReturnedItems(returnedItems, intoWebView: selectedWebView, completion: { (success, returnedItemsError) -> Void in
if !success {
log.error("Failed to fill item into webview: \(returnedItemsError ??? "nil").")
}
})
}
}