From 3cb6ee35aba2ccda54c806a72da4f079fdb8c9fa Mon Sep 17 00:00:00 2001 From: Daniel Jalkut Date: Tue, 19 Jun 2018 13:18:36 -0400 Subject: [PATCH 01/10] First stab at a new 'Subscribe to Feed' Safari App Extension. --- Evergreen.xcodeproj/project.pbxproj | 143 ++++++++++++++++++ .../SafariExtensionViewController.xib | 30 ++++ Safari Extension/Info.plist | 64 ++++++++ Safari Extension/SafariExtensionHandler.swift | 116 ++++++++++++++ .../SafariExtensionViewController.swift | 20 +++ .../Subscribe_to_Feed.entitlements | 8 + Safari Extension/ToolbarItemIcon.pdf | Bin 0 -> 4079 bytes Safari Extension/ToolbarItemIcon.sketch | Bin 0 -> 31857 bytes .../evergreen-subscribe-to-feed.js | 124 +++++++++++++++ 9 files changed, 505 insertions(+) create mode 100644 Safari Extension/Base.lproj/SafariExtensionViewController.xib create mode 100644 Safari Extension/Info.plist create mode 100644 Safari Extension/SafariExtensionHandler.swift create mode 100644 Safari Extension/SafariExtensionViewController.swift create mode 100644 Safari Extension/Subscribe_to_Feed.entitlements create mode 100644 Safari Extension/ToolbarItemIcon.pdf create mode 100644 Safari Extension/ToolbarItemIcon.sketch create mode 100644 Safari Extension/evergreen-subscribe-to-feed.js diff --git a/Evergreen.xcodeproj/project.pbxproj b/Evergreen.xcodeproj/project.pbxproj index f0131b965..aeb94f17e 100644 --- a/Evergreen.xcodeproj/project.pbxproj +++ b/Evergreen.xcodeproj/project.pbxproj @@ -7,6 +7,12 @@ objects = { /* Begin PBXBuildFile section */ + 6581C73820CED60100F4AD34 /* SafariExtensionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6581C73720CED60100F4AD34 /* SafariExtensionHandler.swift */; }; + 6581C73A20CED60100F4AD34 /* SafariExtensionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6581C73920CED60100F4AD34 /* SafariExtensionViewController.swift */; }; + 6581C73D20CED60100F4AD34 /* SafariExtensionViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 6581C73B20CED60100F4AD34 /* SafariExtensionViewController.xib */; }; + 6581C74020CED60100F4AD34 /* evergreen-subscribe-to-feed.js in Resources */ = {isa = PBXBuildFile; fileRef = 6581C73F20CED60100F4AD34 /* evergreen-subscribe-to-feed.js */; }; + 6581C74220CED60100F4AD34 /* ToolbarItemIcon.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 6581C74120CED60100F4AD34 /* ToolbarItemIcon.pdf */; }; + 6581C74620CED60100F4AD34 /* Subscribe to Feed.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 6581C73320CED60000F4AD34 /* Subscribe to Feed.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 840D617F2029031C009BC708 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 840D617E2029031C009BC708 /* AppDelegate.swift */; }; 840D61812029031C009BC708 /* MasterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 840D61802029031C009BC708 /* MasterViewController.swift */; }; 840D61832029031C009BC708 /* DetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 840D61822029031C009BC708 /* DetailViewController.swift */; }; @@ -203,6 +209,13 @@ /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ + 6581C74420CED60100F4AD34 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 849C64581ED37A5D003D8FC0 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 6581C73220CED60000F4AD34; + remoteInfo = "Subscribe to Feed"; + }; 840D61922029031D009BC708 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 849C64581ED37A5D003D8FC0 /* Project object */; @@ -458,6 +471,17 @@ /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ + 6581C75720CED60100F4AD34 /* Embed App Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + 6581C74620CED60100F4AD34 /* Subscribe to Feed.appex in Embed App Extensions */, + ); + name = "Embed App Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; 84B06F681ED37B9000F0B54B /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; @@ -506,6 +530,15 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 6581C73320CED60000F4AD34 /* Subscribe to Feed.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Subscribe to Feed.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; + 6581C73420CED60100F4AD34 /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; }; + 6581C73720CED60100F4AD34 /* SafariExtensionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariExtensionHandler.swift; sourceTree = ""; }; + 6581C73920CED60100F4AD34 /* SafariExtensionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariExtensionViewController.swift; sourceTree = ""; }; + 6581C73C20CED60100F4AD34 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/SafariExtensionViewController.xib; sourceTree = ""; }; + 6581C73E20CED60100F4AD34 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 6581C73F20CED60100F4AD34 /* evergreen-subscribe-to-feed.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = "evergreen-subscribe-to-feed.js"; sourceTree = ""; }; + 6581C74120CED60100F4AD34 /* ToolbarItemIcon.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = ToolbarItemIcon.pdf; sourceTree = ""; }; + 6581C74320CED60100F4AD34 /* Subscribe_to_Feed.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Subscribe_to_Feed.entitlements; sourceTree = ""; }; 8403E75A201C4A79007F7246 /* FeedListKeyboardDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedListKeyboardDelegate.swift; sourceTree = ""; }; 840D617C2029031C009BC708 /* Evergreen.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Evergreen.app; sourceTree = BUILT_PRODUCTS_DIR; }; 840D617E2029031C009BC708 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; @@ -700,6 +733,14 @@ /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 6581C73020CED60000F4AD34 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 6581C73520CED60100F4AD34 /* Cocoa.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 840D61792029031C009BC708 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -749,6 +790,20 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 6581C73620CED60100F4AD34 /* Safari Extension */ = { + isa = PBXGroup; + children = ( + 6581C73720CED60100F4AD34 /* SafariExtensionHandler.swift */, + 6581C73920CED60100F4AD34 /* SafariExtensionViewController.swift */, + 6581C73B20CED60100F4AD34 /* SafariExtensionViewController.xib */, + 6581C73E20CED60100F4AD34 /* Info.plist */, + 6581C73F20CED60100F4AD34 /* evergreen-subscribe-to-feed.js */, + 6581C74120CED60100F4AD34 /* ToolbarItemIcon.pdf */, + 6581C74320CED60100F4AD34 /* Subscribe_to_Feed.entitlements */, + ); + path = "Safari Extension"; + sourceTree = ""; + }; 840D617D2029031C009BC708 /* Evergreen-iOS */ = { isa = PBXGroup; children = ( @@ -1120,6 +1175,7 @@ 840D617D2029031C009BC708 /* Evergreen-iOS */, 840D61942029031D009BC708 /* Evergreen-iOSTests */, 840D619F2029031E009BC708 /* Evergreen-iOSUITests */, + 6581C73620CED60100F4AD34 /* Safari Extension */, 84FB9A2C1EDCD6A4003D53B9 /* Frameworks */, 849C64741ED37A5D003D8FC0 /* EvergreenTests */, D5907CDA2002F084005947E5 /* xcconfig */, @@ -1145,6 +1201,7 @@ 840D617C2029031C009BC708 /* Evergreen.app */, 840D61912029031D009BC708 /* Evergreen-iOSTests.xctest */, 840D619C2029031D009BC708 /* Evergreen-iOSUITests.xctest */, + 6581C73320CED60000F4AD34 /* Subscribe to Feed.appex */, ); name = Products; sourceTree = ""; @@ -1307,6 +1364,7 @@ children = ( 847752FE2008879500D93690 /* CoreServices.framework */, 84FB9A2D1EDCD6B8003D53B9 /* Sparkle.framework */, + 6581C73420CED60100F4AD34 /* Cocoa.framework */, ); name = Frameworks; path = Evergreen/Extensions; @@ -1387,6 +1445,23 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + 6581C73220CED60000F4AD34 /* Subscribe to Feed */ = { + isa = PBXNativeTarget; + buildConfigurationList = 6581C75620CED60100F4AD34 /* Build configuration list for PBXNativeTarget "Subscribe to Feed" */; + buildPhases = ( + 6581C72F20CED60000F4AD34 /* Sources */, + 6581C73020CED60000F4AD34 /* Frameworks */, + 6581C73120CED60000F4AD34 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "Subscribe to Feed"; + productName = "Subscribe to Feed"; + productReference = 6581C73320CED60000F4AD34 /* Subscribe to Feed.appex */; + productType = "com.apple.product-type.app-extension"; + }; 840D617B2029031C009BC708 /* Evergreen-iOS */ = { isa = PBXNativeTarget; buildConfigurationList = 840D61A32029031E009BC708 /* Build configuration list for PBXNativeTarget "Evergreen-iOS" */; @@ -1449,6 +1524,7 @@ 849C645E1ED37A5D003D8FC0 /* Resources */, 84C987A52000AC9E0066B150 /* ShellScript */, 84B06F681ED37B9000F0B54B /* Embed Frameworks */, + 6581C75720CED60100F4AD34 /* Embed App Extensions */, ); buildRules = ( ); @@ -1463,6 +1539,7 @@ 84BB4B7A1F11753300858766 /* PBXTargetDependency */, 846E77401F6EF67A00A165E2 /* PBXTargetDependency */, 846E77441F6EF6A100A165E2 /* PBXTargetDependency */, + 6581C74520CED60100F4AD34 /* PBXTargetDependency */, ); name = Evergreen; productName = Evergreen; @@ -1588,6 +1665,7 @@ 840D617B2029031C009BC708 /* Evergreen-iOS */, 840D61902029031D009BC708 /* Evergreen-iOSTests */, 840D619B2029031D009BC708 /* Evergreen-iOSUITests */, + 6581C73220CED60000F4AD34 /* Subscribe to Feed */, ); }; /* End PBXProject section */ @@ -1757,6 +1835,16 @@ /* End PBXReferenceProxy section */ /* Begin PBXResourcesBuildPhase section */ + 6581C73120CED60000F4AD34 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 6581C74220CED60100F4AD34 /* ToolbarItemIcon.pdf in Resources */, + 6581C73D20CED60100F4AD34 /* SafariExtensionViewController.xib in Resources */, + 6581C74020CED60100F4AD34 /* evergreen-subscribe-to-feed.js in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 840D617A2029031C009BC708 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -1834,6 +1922,15 @@ /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + 6581C72F20CED60000F4AD34 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 6581C73A20CED60100F4AD34 /* SafariExtensionViewController.swift in Sources */, + 6581C73820CED60100F4AD34 /* SafariExtensionHandler.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 840D61782029031C009BC708 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -2005,6 +2102,11 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ + 6581C74520CED60100F4AD34 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 6581C73220CED60000F4AD34 /* Subscribe to Feed */; + targetProxy = 6581C74420CED60100F4AD34 /* PBXContainerItemProxy */; + }; 840D61932029031D009BC708 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 840D617B2029031C009BC708 /* Evergreen-iOS */; @@ -2073,6 +2175,14 @@ /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ + 6581C73B20CED60100F4AD34 /* SafariExtensionViewController.xib */ = { + isa = PBXVariantGroup; + children = ( + 6581C73C20CED60100F4AD34 /* Base */, + ); + name = SafariExtensionViewController.xib; + sourceTree = ""; + }; 840D61842029031C009BC708 /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( @@ -2149,6 +2259,30 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ + 6581C74720CED60100F4AD34 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D5907CE02002F0FA005947E5 /* Evergreen_target.xcconfig */; + buildSettings = { + CODE_SIGN_ENTITLEMENTS = "Safari Extension/Subscribe_to_Feed.entitlements"; + INFOPLIST_FILE = "Safari Extension/Info.plist"; + PRODUCT_BUNDLE_IDENTIFIER = "com.ranchero.Evergreen.Subscribe-to-Feed"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + }; + name = Debug; + }; + 6581C74820CED60100F4AD34 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D5907CE02002F0FA005947E5 /* Evergreen_target.xcconfig */; + buildSettings = { + CODE_SIGN_ENTITLEMENTS = "Safari Extension/Subscribe_to_Feed.entitlements"; + INFOPLIST_FILE = "Safari Extension/Info.plist"; + PRODUCT_BUNDLE_IDENTIFIER = "com.ranchero.Evergreen.Subscribe-to-Feed"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + }; + name = Release; + }; 840D61A42029031E009BC708 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -2595,6 +2729,15 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 6581C75620CED60100F4AD34 /* Build configuration list for PBXNativeTarget "Subscribe to Feed" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 6581C74720CED60100F4AD34 /* Debug */, + 6581C74820CED60100F4AD34 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 840D61A32029031E009BC708 /* Build configuration list for PBXNativeTarget "Evergreen-iOS" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/Safari Extension/Base.lproj/SafariExtensionViewController.xib b/Safari Extension/Base.lproj/SafariExtensionViewController.xib new file mode 100644 index 000000000..6c80db684 --- /dev/null +++ b/Safari Extension/Base.lproj/SafariExtensionViewController.xib @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Safari Extension/Info.plist b/Safari Extension/Info.plist new file mode 100644 index 000000000..4d68d1dd6 --- /dev/null +++ b/Safari Extension/Info.plist @@ -0,0 +1,64 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Subscribe to Feed + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + XPC! + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSExtension + + NSExtensionPointIdentifier + com.apple.Safari.extension + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).SafariExtensionHandler + SFSafariContentScript + + + Script + evergreen-subscribe-to-feed.js + + + SFSafariToolbarItem + + Action + Command + Identifier + Button + Image + ToolbarItemIcon.pdf + Label + Subscribe to Feed + + SFSafariWebsiteAccess + + Allowed Domains + + *.* + + Level + All + + + NSHumanReadableCopyright + Copyright © 2018 Ranchero Software. All rights reserved. + NSHumanReadableDescription + This extension adds a Safari toolbar button for easily subscribing to the syndication feed for the current page. + + diff --git a/Safari Extension/SafariExtensionHandler.swift b/Safari Extension/SafariExtensionHandler.swift new file mode 100644 index 000000000..f1f428376 --- /dev/null +++ b/Safari Extension/SafariExtensionHandler.swift @@ -0,0 +1,116 @@ +// +// SafariExtensionHandler.swift +// Subscribe to Feed +// +// Created by Daniel Jalkut on 6/11/18. +// Copyright © 2018 Ranchero Software. All rights reserved. +// + +import SafariServices + +class SafariExtensionHandler: SFSafariExtensionHandler { + + // Safari App Extensions don't support any reasonable means of detecting whether a + // specific Safari page was loaded with the benefit of the extension's injected + // JavaScript. For this reason a condition can easily be reached where the toolbar + // icon is active for a page, but the expected supporting code is not loaded into + // the page. To detect this and disable our icon, we use a kind of "ping" trick + // to verify whether our code is installed. + + // I tried to use a NSMapTable from String to the closure directly, but Swift + // complains that the object as to be a class type. + typealias ValidationHandler = (Bool, String) -> Void + class ValidationWrapper { + let validationHandler: ValidationHandler + + init(validationHandler: @escaping ValidationHandler) { + self.validationHandler = validationHandler + } + } + + // Maps from UUID to a validation wrapper + static var gPingPongMap = Dictionary() + + // Bottleneck for calling through to a validation handler we have saved, and removing it from the list. + static func callValidationHandler(forHandlerID handlerID: String, withShouldValidate shouldValidate: Bool) { + if let validationWrapper = gPingPongMap[handlerID] { + validationWrapper.validationHandler(shouldValidate, "") + gPingPongMap.removeValue(forKey: handlerID) + } + } + + var validationQueue = DispatchQueue(label: "Toolbar Validation") + + override func messageReceived(withName messageName: String, from page: SFSafariPage, userInfo: [String : Any]?) { + if (messageName == "subscribeToFeed") { + if let feedURLString = userInfo?["url"] as? String { + if let feedURL = URL(string: feedURLString) { + // We could do something more Evergreen-specific like invoke an app-specific scheme + // to subscribe in the app. For starters we just let NSWorkspace open the URL in the + // default "feed:" URL scheme handler. + NSWorkspace.shared.open(feedURL) + } + } + } + else if (messageName == "pong") { + if let validationIDString = userInfo?["validationID"] as? String { + // Should we validate the button? + let shouldValidate = userInfo?["shouldValidate"] as? Bool ?? false + SafariExtensionHandler.callValidationHandler(forHandlerID: validationIDString, withShouldValidate:shouldValidate) + } + } + } + + override func toolbarItemClicked(in window: SFSafariWindow) { + window.getActiveTab { (activeTab) in + activeTab?.getActivePage(completionHandler: { (activePage) in + activePage?.dispatchMessageToScript(withName: "toolbarButtonClicked", userInfo: nil) + }) + } + } + + override func validateToolbarItem(in window: SFSafariWindow, validationHandler: @escaping ((Bool, String) -> Void)) { + + let uniqueValidationID = NSUUID().uuidString + + // Save it right away to eliminate any doubt of whether the handler gets deallocated while + // we are waiting for a callback from the getActiveTab or getActivatePage methods below. + let validationWrapper = ValidationWrapper(validationHandler: validationHandler) + SafariExtensionHandler.gPingPongMap[uniqueValidationID] = validationWrapper + + validationQueue.sync { + // See comments above where gPingPongMap is declared. Upon being asked to validate the + // toolbar icon for a specific page, we save the validationHandler and postpone calling + // it until we have either received a response from our installed JavaScript, or until + // a timeout period has elapsed + window.getActiveTab { (activeTab) in + guard let activeTab = activeTab else { + SafariExtensionHandler.callValidationHandler(forHandlerID: uniqueValidationID, withShouldValidate:false); + return + } + + activeTab.getActivePage { (activePage) in + guard let activePage = activePage else { + SafariExtensionHandler.callValidationHandler(forHandlerID: uniqueValidationID, withShouldValidate:false); + return + } + + activePage.getPropertiesWithCompletionHandler { (pageProperties) in + if let isActive = pageProperties?.isActive { + if isActive { + // Capture the uniqueValidationID to ensure it doesn't change out from under us on a future call + activePage.dispatchMessageToScript(withName: "ping", userInfo: ["validationID": uniqueValidationID]) + + let pongTimeoutInNanoseconds = Int(NSEC_PER_SEC / UInt64(1)) + let timeoutDeadline = DispatchTime.now() + DispatchTimeInterval.nanoseconds(pongTimeoutInNanoseconds) + DispatchQueue.main.asyncAfter(deadline: timeoutDeadline, execute: { [timedOutValidationID = uniqueValidationID] in + SafariExtensionHandler.callValidationHandler(forHandlerID: timedOutValidationID, withShouldValidate:false) + }) + } + } + } + } + } + } + } +} diff --git a/Safari Extension/SafariExtensionViewController.swift b/Safari Extension/SafariExtensionViewController.swift new file mode 100644 index 000000000..aac562ef0 --- /dev/null +++ b/Safari Extension/SafariExtensionViewController.swift @@ -0,0 +1,20 @@ +// +// SafariExtensionViewController.swift +// Subscribe to Feed +// +// Created by Daniel Jalkut on 6/11/18. +// Copyright © 2018 Ranchero Software. All rights reserved. +// + +import SafariServices + +class SafariExtensionViewController: SFSafariExtensionViewController { + + // This would be the place to handle a popover that could, for example, list the possibly multiple feeds offered by a site. + static let shared: SafariExtensionViewController = { + let shared = SafariExtensionViewController() + shared.preferredContentSize = NSSize(width:320, height:240) + return shared + }() + +} diff --git a/Safari Extension/Subscribe_to_Feed.entitlements b/Safari Extension/Subscribe_to_Feed.entitlements new file mode 100644 index 000000000..852fa1a47 --- /dev/null +++ b/Safari Extension/Subscribe_to_Feed.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.app-sandbox + + + diff --git a/Safari Extension/ToolbarItemIcon.pdf b/Safari Extension/ToolbarItemIcon.pdf new file mode 100644 index 0000000000000000000000000000000000000000..07eb6c7434bd40d94bc98e964319e6829d099607 GIT binary patch literal 4079 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%0D!0l&9(x4GeJ=jJ2g?djB>rQ`8N^(0hty7n5 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%0AX?5y>D+vPKbQ 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 bzW@6}A 0)) + { + feedToOpen = thisPageLinkObjects[0]; + subscribeToFeed(feedToOpen); + } + } + else if (event.name === "ping") + { + // Just a hack to get the toolbar icon validation to work as expected. + // If we don't pong back, the extension knows we are not loaded in a page. + + // There is a bug in Safari where the messageHandler is apparently held on to by Safari + // even after an extension is disabled. So an effort to "ping" an extension's scripts will + // succeed even if its been disabled and the page reloaded. Checking for the existance of + // document.location seems to ensure we have enough of a handle still on the document that + // we can do something useful with it. + var shouldValidate = (document.location != null) && (thisPageLinkObjects.length > 0); + + // Pass back the same validationID we were handed so they can look up the correlated validationHandler + safari.extension.dispatchMessage("pong", { "validationID": event.message.validationID, "shouldValidate": thisPageLinkObjects.length > 0 }); + } + }, false); + + scanForSyndicationFeeds(); + } + }); +} From 86ba4b84a84d0ed510196d03f62839594e031c5f Mon Sep 17 00:00:00 2001 From: Daniel Jalkut Date: Tue, 19 Jun 2018 14:41:50 -0400 Subject: [PATCH 02/10] use the pre-calculated validation test. --- Safari Extension/evergreen-subscribe-to-feed.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Safari Extension/evergreen-subscribe-to-feed.js b/Safari Extension/evergreen-subscribe-to-feed.js index e75d43e8f..5e3e1f1ca 100644 --- a/Safari Extension/evergreen-subscribe-to-feed.js +++ b/Safari Extension/evergreen-subscribe-to-feed.js @@ -114,7 +114,7 @@ if ((window.top === window) && (typeof safari != 'undefined') && (document.loca var shouldValidate = (document.location != null) && (thisPageLinkObjects.length > 0); // Pass back the same validationID we were handed so they can look up the correlated validationHandler - safari.extension.dispatchMessage("pong", { "validationID": event.message.validationID, "shouldValidate": thisPageLinkObjects.length > 0 }); + safari.extension.dispatchMessage("pong", { "validationID": event.message.validationID, "shouldValidate": shouldValidate }); } }, false); From d01163b6d45347f155fabfecd106dde970d3087e Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Wed, 8 Aug 2018 22:33:47 -0700 Subject: [PATCH 03/10] Update RSDatabase. --- submodules/RSDatabase | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/submodules/RSDatabase b/submodules/RSDatabase index 8a346eff6..61a63064a 160000 --- a/submodules/RSDatabase +++ b/submodules/RSDatabase @@ -1 +1 @@ -Subproject commit 8a346eff6ad6a529c39b736ea802182b5edd3f5f +Subproject commit 61a63064a54578c8c72b84f3756b72a651a3bf16 From 6fa34df300e2cb908806828e71a07ff2afbd6adf Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Sat, 18 Aug 2018 11:04:28 -0700 Subject: [PATCH 04/10] Add note about what to do with submodules on first checkout. --- Technotes/SubmoduleCheatSheet.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Technotes/SubmoduleCheatSheet.md b/Technotes/SubmoduleCheatSheet.md index 6f1ce732b..e6c62a52d 100644 --- a/Technotes/SubmoduleCheatSheet.md +++ b/Technotes/SubmoduleCheatSheet.md @@ -2,6 +2,12 @@ Evergreen uses [Git submodules](https://git-scm.com/book/en/v2/Git-Tools-Submodules) to include shared frameworks. At this writing (June 2018) they are DB5, RSCore, RSDatabase, RSWeb, RSTree, and RSParser. +After your first checkout: + + git submodule init + git submodule update + + To add a submodule: git submodule add https://github.com/username/path From 61a390a947bf4f609e42d6a5f5d0f47a9fb36706 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Sat, 18 Aug 2018 11:15:24 -0700 Subject: [PATCH 05/10] Unbreak the build. I think. --- Evergreen/MainWindow/Timeline/Cell/UnreadIndicatorView.swift | 2 +- submodules/RSDatabase | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Evergreen/MainWindow/Timeline/Cell/UnreadIndicatorView.swift b/Evergreen/MainWindow/Timeline/Cell/UnreadIndicatorView.swift index c139c8d10..990324b7d 100644 --- a/Evergreen/MainWindow/Timeline/Cell/UnreadIndicatorView.swift +++ b/Evergreen/MainWindow/Timeline/Cell/UnreadIndicatorView.swift @@ -38,7 +38,7 @@ class UnreadIndicatorView: NSView { override func draw(_ dirtyRect: NSRect) { if #available(OSX 10.14, *) { - let color = isEmphasized && isSelected ? NSColor.white : NSColor.controlAccent + let color = isEmphasized && isSelected ? NSColor.white : NSColor.controlAccentColor color.setFill() } else { let color = isEmphasized && isSelected ? NSColor.white : NSColor.systemBlue diff --git a/submodules/RSDatabase b/submodules/RSDatabase index 61a63064a..29c89417a 160000 --- a/submodules/RSDatabase +++ b/submodules/RSDatabase @@ -1 +1 @@ -Subproject commit 61a63064a54578c8c72b84f3756b72a651a3bf16 +Subproject commit 29c89417a7138e125ed0a076ba8d6f83b09aa522 From 8748e6f8cff86f6bdd21da7cf19af4fddab7c41e Mon Sep 17 00:00:00 2001 From: Daniel Jalkut Date: Sun, 19 Aug 2018 14:08:31 -0400 Subject: [PATCH 06/10] Oops - need to multiply instead of divide to get accurate representation of delay timeout in nanoseconds. --- Safari Extension/SafariExtensionHandler.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Safari Extension/SafariExtensionHandler.swift b/Safari Extension/SafariExtensionHandler.swift index f1f428376..19dc679d9 100644 --- a/Safari Extension/SafariExtensionHandler.swift +++ b/Safari Extension/SafariExtensionHandler.swift @@ -101,7 +101,7 @@ class SafariExtensionHandler: SFSafariExtensionHandler { // Capture the uniqueValidationID to ensure it doesn't change out from under us on a future call activePage.dispatchMessageToScript(withName: "ping", userInfo: ["validationID": uniqueValidationID]) - let pongTimeoutInNanoseconds = Int(NSEC_PER_SEC / UInt64(1)) + let pongTimeoutInNanoseconds = Int(NSEC_PER_SEC * UInt64(1)) let timeoutDeadline = DispatchTime.now() + DispatchTimeInterval.nanoseconds(pongTimeoutInNanoseconds) DispatchQueue.main.asyncAfter(deadline: timeoutDeadline, execute: { [timedOutValidationID = uniqueValidationID] in SafariExtensionHandler.callValidationHandler(forHandlerID: timedOutValidationID, withShouldValidate:false) From ccc111b152b1c48caf75c079cfe419d640522fe7 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Sun, 19 Aug 2018 12:18:33 -0700 Subject: [PATCH 07/10] Add ArticlesDatabase.framework to embedded frameworks. --- Evergreen.xcodeproj/project.pbxproj | 36 ++++++++++++++++++----------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/Evergreen.xcodeproj/project.pbxproj b/Evergreen.xcodeproj/project.pbxproj index 68eb069f6..238ab8f0d 100644 --- a/Evergreen.xcodeproj/project.pbxproj +++ b/Evergreen.xcodeproj/project.pbxproj @@ -38,6 +38,8 @@ 842E45E51ED8C6B7000A8B52 /* MainWindowSplitView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842E45E41ED8C6B7000A8B52 /* MainWindowSplitView.swift */; }; 842E45E71ED8C747000A8B52 /* DB5.plist in Resources */ = {isa = PBXBuildFile; fileRef = 842E45E61ED8C747000A8B52 /* DB5.plist */; }; 843A3B5620311E7700BF76EC /* FeedListOutlineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843A3B5520311E7700BF76EC /* FeedListOutlineView.swift */; }; + 8440C8AD2129F9F5002353D1 /* ArticlesDatabase.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 841D4D682106B3E100DD04E6 /* ArticlesDatabase.framework */; }; + 8440C8AE2129F9F5002353D1 /* ArticlesDatabase.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 841D4D682106B3E100DD04E6 /* ArticlesDatabase.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 84411E711FE5FBFA004B527F /* SmallIconProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84411E701FE5FBFA004B527F /* SmallIconProvider.swift */; }; 8444C8F21FED81840051386C /* OPMLExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8444C8F11FED81840051386C /* OPMLExporter.swift */; }; 844B5B591FE9FE4F00C7C76A /* SidebarKeyboardDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 844B5B581FE9FE4F00C7C76A /* SidebarKeyboardDelegate.swift */; }; @@ -210,13 +212,6 @@ /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ - 6581C74420CED60100F4AD34 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 849C64581ED37A5D003D8FC0 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 6581C73220CED60000F4AD34; - remoteInfo = "Subscribe to Feed"; - }; 840D61922029031D009BC708 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 849C64581ED37A5D003D8FC0 /* Project object */; @@ -266,6 +261,13 @@ remoteGlobalIDString = 844BEE5A1F0AB3C8004AB7CD; remoteInfo = Articles; }; + 8440C8AF2129F9F5002353D1 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 841D4D5E2106B3E100DD04E6 /* ArticlesDatabase.xcodeproj */; + proxyType = 1; + remoteGlobalIDString = 844BEE361F0AB3AA004AB7CD; + remoteInfo = ArticlesDatabase; + }; 846E77391F6EF5D700A165E2 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 846E77301F6EF5D600A165E2 /* Account.xcodeproj */; @@ -469,6 +471,7 @@ 84C37FB620DD8DBB00CA8CF5 /* RSParser.framework in Embed Frameworks */, 84C37FA620DD8D8400CA8CF5 /* RSCore.framework in Embed Frameworks */, 846E773E1F6EF67A00A165E2 /* Account.framework in Embed Frameworks */, + 8440C8AE2129F9F5002353D1 /* ArticlesDatabase.framework in Embed Frameworks */, 841D4D6C2106B3ED00DD04E6 /* Articles.framework in Embed Frameworks */, ); name = "Embed Frameworks"; @@ -709,7 +712,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 6581C73520CED60100F4AD34 /* Cocoa.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -746,6 +748,7 @@ 846E773D1F6EF67A00A165E2 /* Account.framework in Frameworks */, 84C37FA520DD8D8400CA8CF5 /* RSCore.framework in Frameworks */, 84FB9A2F1EDCD6C4003D53B9 /* Sparkle.framework in Frameworks */, + 8440C8AD2129F9F5002353D1 /* ArticlesDatabase.framework in Frameworks */, 841D4D6B2106B3ED00DD04E6 /* Articles.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1508,6 +1511,7 @@ 84C37FB820DD8DBB00CA8CF5 /* PBXTargetDependency */, 84C37FC820DD8E1D00CA8CF5 /* PBXTargetDependency */, 841D4D6E2106B3ED00DD04E6 /* PBXTargetDependency */, + 8440C8B02129F9F5002353D1 /* PBXTargetDependency */, ); name = Evergreen; productName = Evergreen; @@ -1543,6 +1547,10 @@ LastUpgradeCheck = 0930; ORGANIZATIONNAME = "Ranchero Software"; TargetAttributes = { + 6581C73220CED60000F4AD34 = { + DevelopmentTeam = M8L2WTLA8W; + ProvisioningStyle = Manual; + }; 840D617B2029031C009BC708 = { CreatedOnToolsVersion = 9.3; DevelopmentTeam = 9C84TZ7Q6Z; @@ -1867,7 +1875,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "# See https://blog.curtisherbert.com/automated-build-numbers/\n\ngit=`sh /etc/profile; which git`\nbranch_name=`$git symbolic-ref HEAD | sed -e 's,.*/\\\\(.*\\\\),\\\\1,'`\ngit_count=`$git rev-list $branch_name |wc -l | sed 's/^ *//;s/ *$//'`\nsimple_branch_name=`$git rev-parse --abbrev-ref HEAD`\n\nbuild_number=\"$git_count\"\nif [ $CONFIGURATION != \"Release\" ]; then\nbuild_number+=\"-$simple_branch_name\"\nfi\n\nplist=\"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}\"\ndsym_plist=\"${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Info.plist\"\n\n/usr/libexec/PlistBuddy -c \"Set :CFBundleVersion $build_number\" \"$plist\"\nif [ -f \"$DSYM_INFO_PLIST\" ] ; then\n/usr/libexec/PlistBuddy -c \"Set :CFBundleVersion $build_number\" \"$dsym_plist\"\nfi"; + shellScript = "# See https://blog.curtisherbert.com/automated-build-numbers/\n\ngit=`sh /etc/profile; which git`\nbranch_name=`$git symbolic-ref HEAD | sed -e 's,.*/\\\\(.*\\\\),\\\\1,'`\ngit_count=`$git rev-list $branch_name |wc -l | sed 's/^ *//;s/ *$//'`\nsimple_branch_name=`$git rev-parse --abbrev-ref HEAD`\n\nbuild_number=\"$git_count\"\nif [ $CONFIGURATION != \"Release\" ]; then\nbuild_number+=\"-$simple_branch_name\"\nfi\n\nplist=\"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}\"\ndsym_plist=\"${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Info.plist\"\n\n/usr/libexec/PlistBuddy -c \"Set :CFBundleVersion $build_number\" \"$plist\"\nif [ -f \"$DSYM_INFO_PLIST\" ] ; then\n/usr/libexec/PlistBuddy -c \"Set :CFBundleVersion $build_number\" \"$dsym_plist\"\nfi\n"; }; /* End PBXShellScriptBuildPhase section */ @@ -2057,11 +2065,6 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ - 6581C74520CED60100F4AD34 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 6581C73220CED60000F4AD34 /* Subscribe to Feed */; - targetProxy = 6581C74420CED60100F4AD34 /* PBXContainerItemProxy */; - }; 840D61932029031D009BC708 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 840D617B2029031C009BC708 /* Evergreen-iOS */; @@ -2077,6 +2080,11 @@ name = Articles; targetProxy = 841D4D6D2106B3ED00DD04E6 /* PBXContainerItemProxy */; }; + 8440C8B02129F9F5002353D1 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = ArticlesDatabase; + targetProxy = 8440C8AF2129F9F5002353D1 /* PBXContainerItemProxy */; + }; 846E77401F6EF67A00A165E2 /* PBXTargetDependency */ = { isa = PBXTargetDependency; name = Account; From 6caf85b598ad93872ecd218117940707b100e197 Mon Sep 17 00:00:00 2001 From: Daniel Jalkut Date: Sun, 19 Aug 2018 17:54:26 -0400 Subject: [PATCH 08/10] I think it's important to have the persistent functions/variables live outside the document loaded handler, so they persist as long as the page is being viewed by the user. --- .../evergreen-subscribe-to-feed.js | 234 +++++++++--------- 1 file changed, 114 insertions(+), 120 deletions(-) diff --git a/Safari Extension/evergreen-subscribe-to-feed.js b/Safari Extension/evergreen-subscribe-to-feed.js index 5e3e1f1ca..576c4c49b 100644 --- a/Safari Extension/evergreen-subscribe-to-feed.js +++ b/Safari Extension/evergreen-subscribe-to-feed.js @@ -1,124 +1,118 @@ -// Prevent injecting the JavaScript in IFRAMES, and from acting before Safari is ready... -if ((window.top === window) && (typeof safari != 'undefined') && (document.location != null)) { - document.addEventListener("DOMContentLoaded", function(event) { - if (window.top === window) - { - var thisPageLinkObjects = null; +var thisPageLinkObjects = null; - // I convert the native "link" node into an object that I can pass out to the global page - function objectFromLink(theLink) - { - var linkObject = new Object(); +// I convert the native "link" node into an object that I can pass out to the global page +function objectFromLink(theLink) { + var linkObject = new Object(); - linkObject.href = theLink.href; - linkObject.type = theLink.type; - linkObject.title = theLink.title; + linkObject.href = theLink.href; + linkObject.type = theLink.type; + linkObject.title = theLink.title; - return linkObject; - } - - // Some sites will list feeds with inappropriate or at least less-than-ideal information - // in the MIME type attribute. We cover some edge cases here that allow to be passed through, - // where they will successfully open as "feed://" URLs in the browser. - function isValidFeedLink(theLink) - { - var isValid = false; - - switch (theLink.type) - { - case "application/atom+xml": - case "application/x.atom+xml": - case "application/rss+xml": - // These types do not require other criteria. - isValid = (theLink.href != null); - - case "text/xml": - case "application/rdf+xml": - // These types require a title that has "RSS" in it. - if (theLink.title && theLink.title.search(/RSS/i) != -1) - { - isValid = (theLink.href != null); - } - } - - return isValid; - } - - function scanForSyndicationFeeds() - { - // In case we don't find any, we establish that we have at least tried by setting the - // variables to empty instead of null. - thisPageLinkObjects = [] - - thisPageLinks = document.getElementsByTagName("link"); - - for (thisLinkIndex = 0; thisLinkIndex < thisPageLinks.length; thisLinkIndex++) - { - var thisLink = thisPageLinks[thisLinkIndex]; - var thisLinkRel = thisLink.getAttribute("rel"); - if (thisLinkRel == "alternate") - { - if (isValidFeedLink(thisLink)) - { - thisPageLinkObjects.push(objectFromLink(thisLink)); - } - } - } - } - - function subscribeToFeed(theFeed) - { - // Convert the URL to a feed:// scheme because Safari - // will refuse to load e.g. a feed that is listed merely - // as "text/xml". We do some preflighting of the link rel - // in the PageLoadEnd.js so we can be more confident it's a - // good feed: URL. - var feedURL = theFeed.href; - if (feedURL.match(/^http[s]?:\/\//)) - { - feedURL = feedURL.replace(/^http[s]?:\/\//, "feed://"); - } - else if (feedURL.match(/^feed:/) == false) - { - feedURL = "feed:" + feedURL; - } - - safari.extension.dispatchMessage("subscribeToFeed", { "url": feedURL }); - } - - safari.self.addEventListener("message", function(event) - { - if (event.name === "toolbarButtonClicked") - { - // Workaround Radar #31182842, in which residual copies of our - // app extension may remain loaded in context of pages in Safari, - // causing multiple responses to broadcast message about toolbar - // button being clicked. In the case of the "extra" injections, - // the document location is null, so we can avoid doing on anything. - if ((document.location != null) && (thisPageLinkObjects.length > 0)) - { - feedToOpen = thisPageLinkObjects[0]; - subscribeToFeed(feedToOpen); - } - } - else if (event.name === "ping") - { - // Just a hack to get the toolbar icon validation to work as expected. - // If we don't pong back, the extension knows we are not loaded in a page. - - // There is a bug in Safari where the messageHandler is apparently held on to by Safari - // even after an extension is disabled. So an effort to "ping" an extension's scripts will - // succeed even if its been disabled and the page reloaded. Checking for the existance of - // document.location seems to ensure we have enough of a handle still on the document that - // we can do something useful with it. - var shouldValidate = (document.location != null) && (thisPageLinkObjects.length > 0); - - // Pass back the same validationID we were handed so they can look up the correlated validationHandler - safari.extension.dispatchMessage("pong", { "validationID": event.message.validationID, "shouldValidate": shouldValidate }); - } - }, false); - - scanForSyndicationFeeds(); - } - }); + return linkObject; } + +// Some sites will list feeds with inappropriate or at least less-than-ideal information +// in the MIME type attribute. We cover some edge cases here that allow to be passed through, +// where they will successfully open as "feed://" URLs in the browser. +function isValidFeedLink(theLink) { + var isValid = false; + + switch (theLink.type) + { + case "application/atom+xml": + case "application/x.atom+xml": + case "application/rss+xml": + // These types do not require other criteria. + isValid = (theLink.href != null); + + case "text/xml": + case "application/rdf+xml": + // These types require a title that has "RSS" in it. + if (theLink.title && theLink.title.search(/RSS/i) != -1) + { + isValid = (theLink.href != null); + } + } + + return isValid; +} + +function scanForSyndicationFeeds() { + // In case we don't find any, we establish that we have at least tried by setting the + // variables to empty instead of null. + thisPageLinkObjects = [] + + thisPageLinks = document.getElementsByTagName("link"); + + for (thisLinkIndex = 0; thisLinkIndex < thisPageLinks.length; thisLinkIndex++) + { + var thisLink = thisPageLinks[thisLinkIndex]; + var thisLinkRel = thisLink.getAttribute("rel"); + if (thisLinkRel == "alternate") + { + if (isValidFeedLink(thisLink)) + { + thisPageLinkObjects.push(objectFromLink(thisLink)); + } + } + } +} + +function subscribeToFeed(theFeed) { + // Convert the URL to a feed:// scheme because Safari + // will refuse to load e.g. a feed that is listed merely + // as "text/xml". We do some preflighting of the link rel + // in the PageLoadEnd.js so we can be more confident it's a + // good feed: URL. + var feedURL = theFeed.href; + if (feedURL.match(/^http[s]?:\/\//)) + { + feedURL = feedURL.replace(/^http[s]?:\/\//, "feed://"); + } + else if (feedURL.match(/^feed:/) == false) + { + feedURL = "feed:" + feedURL; + } + + safari.extension.dispatchMessage("subscribeToFeed", { "url": feedURL }); +} + +function messageHandler(event) { + if (event.name === "toolbarButtonClicked") + { + // Workaround Radar #31182842, in which residual copies of our + // app extension may remain loaded in context of pages in Safari, + // causing multiple responses to broadcast message about toolbar + // button being clicked. In the case of the "extra" injections, + // the document location is null, so we can avoid doing on anything. + if ((document.location != null) && (thisPageLinkObjects.length > 0)) + { + feedToOpen = thisPageLinkObjects[0]; + subscribeToFeed(feedToOpen); + } + } + else if (event.name === "ping") + { + // Just a hack to get the toolbar icon validation to work as expected. + // If we don't pong back, the extension knows we are not loaded in a page. + + // There is a bug in Safari where the messageHandler is apparently held on to by Safari + // even after an extension is disabled. So an effort to "ping" an extension's scripts will + // succeed even if its been disabled and the page reloaded. Checking for the existance of + // document.location seems to ensure we have enough of a handle still on the document that + // we can do something useful with it. + var shouldValidate = (document.location != null) && (thisPageLinkObjects.length > 0); + + // Pass back the same validationID we were handed so they can look up the correlated validationHandler + safari.extension.dispatchMessage("pong", { "validationID": event.message.validationID, "shouldValidate": shouldValidate }); + } +} + +document.addEventListener("DOMContentLoaded", function(event) { + // Prevent injecting the JavaScript in IFRAMES, and from acting before Safari is ready... + if ((window.top === window) && (typeof safari != 'undefined') && (document.location != null)) + { + safari.self.addEventListener("message", messageHandler, false) + scanForSyndicationFeeds(); + } +}); From ab810e9f1c88a154a7b53385adff9462775866ba Mon Sep 17 00:00:00 2001 From: Daniel Jalkut Date: Sun, 19 Aug 2018 17:55:46 -0400 Subject: [PATCH 09/10] Change the validation code to always discard any pending requests for validation, and focus on the latest request. This works well for our purposes and avoids us queueing up a validation request and then having it somehow come in later than a more up-to-date request that may pertain to a different window or tab. --- Safari Extension/SafariExtensionHandler.swift | 30 +++++++++++++------ 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/Safari Extension/SafariExtensionHandler.swift b/Safari Extension/SafariExtensionHandler.swift index 19dc679d9..f545ad188 100644 --- a/Safari Extension/SafariExtensionHandler.swift +++ b/Safari Extension/SafariExtensionHandler.swift @@ -18,7 +18,7 @@ class SafariExtensionHandler: SFSafariExtensionHandler { // to verify whether our code is installed. // I tried to use a NSMapTable from String to the closure directly, but Swift - // complains that the object as to be a class type. + // complains that the object has to be a class type. typealias ValidationHandler = (Bool, String) -> Void class ValidationWrapper { let validationHandler: ValidationHandler @@ -30,6 +30,7 @@ class SafariExtensionHandler: SFSafariExtensionHandler { // Maps from UUID to a validation wrapper static var gPingPongMap = Dictionary() + static var validationQueue = DispatchQueue(label: "Toolbar Validation") // Bottleneck for calling through to a validation handler we have saved, and removing it from the list. static func callValidationHandler(forHandlerID handlerID: String, withShouldValidate shouldValidate: Bool) { @@ -39,8 +40,6 @@ class SafariExtensionHandler: SFSafariExtensionHandler { } } - var validationQueue = DispatchQueue(label: "Toolbar Validation") - override func messageReceived(withName messageName: String, from page: SFSafariPage, userInfo: [String : Any]?) { if (messageName == "subscribeToFeed") { if let feedURLString = userInfo?["url"] as? String { @@ -73,12 +72,25 @@ class SafariExtensionHandler: SFSafariExtensionHandler { let uniqueValidationID = NSUUID().uuidString - // Save it right away to eliminate any doubt of whether the handler gets deallocated while - // we are waiting for a callback from the getActiveTab or getActivatePage methods below. - let validationWrapper = ValidationWrapper(validationHandler: validationHandler) - SafariExtensionHandler.gPingPongMap[uniqueValidationID] = validationWrapper + SafariExtensionHandler.validationQueue.sync { + + // Save it right away to eliminate any doubt of whether the handler gets deallocated while + // we are waiting for a callback from the getActiveTab or getActivatePage methods below. + let validationWrapper = ValidationWrapper(validationHandler: validationHandler) + SafariExtensionHandler.gPingPongMap[uniqueValidationID] = validationWrapper + + // To avoid problems with validation handlers dispatched after we've, for example, + // switched to a new tab, we aggressively clear out the map of any pending validations, + // and focus only on the newest validation request we've been asked for. + for thisValidationID in SafariExtensionHandler.gPingPongMap.keys { + if thisValidationID != uniqueValidationID { + // Default to valid ... we'll know soon enough whether the latest state + // is actually still valid or not... + SafariExtensionHandler.callValidationHandler(forHandlerID: thisValidationID, withShouldValidate: true); + + } + } - validationQueue.sync { // See comments above where gPingPongMap is declared. Upon being asked to validate the // toolbar icon for a specific page, we save the validationHandler and postpone calling // it until we have either received a response from our installed JavaScript, or until @@ -101,7 +113,7 @@ class SafariExtensionHandler: SFSafariExtensionHandler { // Capture the uniqueValidationID to ensure it doesn't change out from under us on a future call activePage.dispatchMessageToScript(withName: "ping", userInfo: ["validationID": uniqueValidationID]) - let pongTimeoutInNanoseconds = Int(NSEC_PER_SEC * UInt64(1)) + let pongTimeoutInNanoseconds = Int(Double(NSEC_PER_SEC) * 0.5) let timeoutDeadline = DispatchTime.now() + DispatchTimeInterval.nanoseconds(pongTimeoutInNanoseconds) DispatchQueue.main.asyncAfter(deadline: timeoutDeadline, execute: { [timedOutValidationID = uniqueValidationID] in SafariExtensionHandler.callValidationHandler(forHandlerID: timedOutValidationID, withShouldValidate:false) From f621f8fff7cc4c1252e255c7f2147026dd7185c0 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Sun, 19 Aug 2018 15:32:19 -0700 Subject: [PATCH 10/10] =?UTF-8?q?Fix=20missing=20setting=20for=20Articles.?= =?UTF-8?q?framework=20=E2=80=94=20fixes=20bug=20doing=20Release=20build.?= =?UTF-8?q?=20(Didn=E2=80=99t=20affect=20Debug=20builds.)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Frameworks/Articles/Articles.xcodeproj/project.pbxproj | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Frameworks/Articles/Articles.xcodeproj/project.pbxproj b/Frameworks/Articles/Articles.xcodeproj/project.pbxproj index e626d849b..8e79303ad 100644 --- a/Frameworks/Articles/Articles.xcodeproj/project.pbxproj +++ b/Frameworks/Articles/Articles.xcodeproj/project.pbxproj @@ -343,6 +343,7 @@ 844BEE701F0AB3C9004AB7CD /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = "$(SRCROOT)/Info.plist"; MACOSX_DEPLOYMENT_TARGET = 10.13; PRODUCT_BUNDLE_IDENTIFIER = com.ranchero.Articles; @@ -353,6 +354,7 @@ 844BEE711F0AB3C9004AB7CD /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = "$(SRCROOT)/Info.plist"; MACOSX_DEPLOYMENT_TARGET = 10.13; PRODUCT_BUNDLE_IDENTIFIER = com.ranchero.Articles;