Merge pull request #384 from danielpunkass/safari-extension

Safari extension for "Subscribe to Feed"
This commit is contained in:
Brent Simmons 2018-08-19 11:46:12 -07:00 committed by GitHub
commit 86b1aef07f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 504 additions and 0 deletions

View File

@ -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 */; };
@ -204,6 +210,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 */;
@ -431,6 +444,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;
@ -477,6 +501,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 = "<group>"; };
6581C73920CED60100F4AD34 /* SafariExtensionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariExtensionViewController.swift; sourceTree = "<group>"; };
6581C73C20CED60100F4AD34 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/SafariExtensionViewController.xib; sourceTree = "<group>"; };
6581C73E20CED60100F4AD34 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
6581C73F20CED60100F4AD34 /* evergreen-subscribe-to-feed.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = "evergreen-subscribe-to-feed.js"; sourceTree = "<group>"; };
6581C74120CED60100F4AD34 /* ToolbarItemIcon.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = ToolbarItemIcon.pdf; sourceTree = "<group>"; };
6581C74320CED60100F4AD34 /* Subscribe_to_Feed.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Subscribe_to_Feed.entitlements; sourceTree = "<group>"; };
8403E75A201C4A79007F7246 /* FeedListKeyboardDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedListKeyboardDelegate.swift; sourceTree = "<group>"; };
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 = "<group>"; };
@ -672,6 +705,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;
@ -719,6 +760,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 = "<group>";
};
840D617D2029031C009BC708 /* Evergreen-iOS */ = {
isa = PBXGroup;
children = (
@ -1090,6 +1145,7 @@
840D617D2029031C009BC708 /* Evergreen-iOS */,
840D61942029031D009BC708 /* Evergreen-iOSTests */,
840D619F2029031E009BC708 /* Evergreen-iOSUITests */,
6581C73620CED60100F4AD34 /* Safari Extension */,
84FB9A2C1EDCD6A4003D53B9 /* Frameworks */,
849C64741ED37A5D003D8FC0 /* EvergreenTests */,
D5907CDA2002F084005947E5 /* xcconfig */,
@ -1114,6 +1170,7 @@
840D617C2029031C009BC708 /* Evergreen.app */,
840D61912029031D009BC708 /* Evergreen-iOSTests.xctest */,
840D619C2029031D009BC708 /* Evergreen-iOSUITests.xctest */,
6581C73320CED60000F4AD34 /* Subscribe to Feed.appex */,
);
name = Products;
sourceTree = "<group>";
@ -1278,6 +1335,7 @@
children = (
847752FE2008879500D93690 /* CoreServices.framework */,
84FB9A2D1EDCD6B8003D53B9 /* Sparkle.framework */,
6581C73420CED60100F4AD34 /* Cocoa.framework */,
);
name = Frameworks;
path = Evergreen/Extensions;
@ -1358,6 +1416,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" */;
@ -1420,6 +1495,7 @@
849C645E1ED37A5D003D8FC0 /* Resources */,
84C987A52000AC9E0066B150 /* ShellScript */,
84B06F681ED37B9000F0B54B /* Embed Frameworks */,
6581C75720CED60100F4AD34 /* Embed App Extensions */,
);
buildRules = (
);
@ -1553,6 +1629,7 @@
840D617B2029031C009BC708 /* Evergreen-iOS */,
840D61902029031D009BC708 /* Evergreen-iOSTests */,
840D619B2029031D009BC708 /* Evergreen-iOSUITests */,
6581C73220CED60000F4AD34 /* Subscribe to Feed */,
);
};
/* End PBXProject section */
@ -1708,6 +1785,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;
@ -1785,6 +1872,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;
@ -1961,6 +2057,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 */;
@ -2019,6 +2120,14 @@
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
6581C73B20CED60100F4AD34 /* SafariExtensionViewController.xib */ = {
isa = PBXVariantGroup;
children = (
6581C73C20CED60100F4AD34 /* Base */,
);
name = SafariExtensionViewController.xib;
sourceTree = "<group>";
};
840D61842029031C009BC708 /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
@ -2095,6 +2204,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 = {
@ -2545,6 +2678,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 = (

View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="11134" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="11134"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="SafariExtensionViewController" customModuleProvider="target">
<connections>
<outlet property="view" destination="c22-O7-iKe" id="vwT-Xx-Aiz"/>
</connections>
</customObject>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<customView id="c22-O7-iKe">
<rect key="frame" x="0.0" y="0.0" width="330" height="119"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<subviews>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="4Iy-aV-wGF">
<rect key="frame" x="103" y="82" width="124" height="17"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Subscribe to Feed" id="2Ec-kd-q2K">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
</customView>
</objects>
</document>

View File

@ -0,0 +1,64 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Subscribe to Feed</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSMinimumSystemVersion</key>
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.Safari.extension</string>
<key>NSExtensionPrincipalClass</key>
<string>$(PRODUCT_MODULE_NAME).SafariExtensionHandler</string>
<key>SFSafariContentScript</key>
<array>
<dict>
<key>Script</key>
<string>evergreen-subscribe-to-feed.js</string>
</dict>
</array>
<key>SFSafariToolbarItem</key>
<dict>
<key>Action</key>
<string>Command</string>
<key>Identifier</key>
<string>Button</string>
<key>Image</key>
<string>ToolbarItemIcon.pdf</string>
<key>Label</key>
<string>Subscribe to Feed</string>
</dict>
<key>SFSafariWebsiteAccess</key>
<dict>
<key>Allowed Domains</key>
<array>
<string>*.*</string>
</array>
<key>Level</key>
<string>All</string>
</dict>
</dict>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2018 Ranchero Software. All rights reserved.</string>
<key>NSHumanReadableDescription</key>
<string>This extension adds a Safari toolbar button for easily subscribing to the syndication feed for the current page.</string>
</dict>
</plist>

View File

@ -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<String, ValidationWrapper>()
// 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)
})
}
}
}
}
}
}
}
}

View File

@ -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
}()
}

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
</dict>
</plist>

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,124 @@
// 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;
// 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;
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();
}
});
}