diff --git a/internal_packages/notification-mailto/lib/launch-services.coffee b/internal_packages/notification-mailto/lib/launch-services.coffee index 74743bc6a..80640c7ca 100644 --- a/internal_packages/notification-mailto/lib/launch-services.coffee +++ b/internal_packages/notification-mailto/lib/launch-services.coffee @@ -2,7 +2,6 @@ exec = require('child_process').exec fs = require('fs') bundleIdentifier = 'com.inbox.edgehill' -launchServicesPlistPath = "#{process.env.HOME}/Library/Preferences/com.apple.LaunchServices/com.apple.launchservices.secure.plist" module.exports = class LaunchServices @@ -16,92 +15,48 @@ class LaunchServices available: -> @getPlatform() is 'darwin' - isYosemiteOrGreater: (callback) -> - fs.exists launchServicesPlistPath, (exists) => - callback(exists) + getLaunchServicesPlistPath: (callback) -> + secure = "#{process.env.HOME}/Library/Preferences/com.apple.LaunchServices/com.apple.launchservices.secure.plist" + insecure = "#{process.env.HOME}/Library/Preferences/com.apple.LaunchServices.plist" + + fs.exists secure, (exists) => + if exists + callback(secure) + else + callback(insecure) readDefaults: (callback) -> - return callback(@_defaults) if @_defaults - @isYosemiteOrGreater (result) => - if result - @_readDefaultsSecure(callback) - else - @_readDefaultsPreYosemite(callback) - - _readDefaultsSecure: (callback) -> - @secure = true - tmpPath = "#{launchServicesPlistPath}.#{Math.random()}" - exec "plutil -convert json \"#{launchServicesPlistPath}\" -o \"#{tmpPath}\"", (err, stdout, stderr) => - return callback(err) if callback and err - fs.readFile tmpPath, (err, data) => + @getLaunchServicesPlistPath (plistPath) => + tmpPath = "#{plistPath}.#{Math.random()}" + exec "plutil -convert json \"#{plistPath}\" -o \"#{tmpPath}\"", (err, stdout, stderr) -> return callback(err) if callback and err - try - data = JSON.parse(data) - callback(data['LSHandlers'], data) - fs.unlink(tmpPath) - catch e - callback(e) if callback and err - - _readDefaultsPreYosemite: (callback) -> - @secure = false - exec "defaults read com.apple.launchservices LSHandlers", (err, stdout, stderr) => - return callback(err) if callback and err - - # Convert the defaults from Apple's plist format into - # JSON. It's nearly the same, just has different delimiters - plist = stdout.toString() - regex = /([a-zA-Z]*) = (.*);/ - while (match = regex.exec(plist)) != null - [text, key, val] = match - val = "\"#{val}\"" unless val[0] is '"' - plist = plist.replace(text, "\"#{key}\":#{val},") - plist = plist.replace(/\(/g, '[') - plist = plist.replace(/\)/g, ']') - plist = plist.replace(/[\s]*,[\s]*\n[\s]*}/g, '\n}') - - json = [] - if plist.length > 0 - json = JSON.parse(plist) - - callback(json) + fs.readFile tmpPath, (err, data) -> + return callback(err) if callback and err + try + data = JSON.parse(data) + callback(data['LSHandlers'], data) + fs.unlink(tmpPath) + catch e + callback(e) if callback and err writeDefaults: (defaults, callback) -> - @_defaults = defaults - if @secure - @_writeDefaultsSecure(defaults, callback) - else - @_writeDefaultsPreYosemite(defaults, callback) + @getLaunchServicesPlistPath (plistPath) -> + tmpPath = "#{plistPath}.#{Math.random()}" + exec "plutil -convert json \"#{plistPath}\" -o \"#{tmpPath}\"", (err, stdout, stderr) -> + return callback(err) if callback and err + try + data = fs.readFileSync(tmpPath) + data = JSON.parse(data) + data['LSHandlers'] = defaults + data = JSON.stringify(data) + fs.writeFileSync(tmpPath, data) + catch error + return callback(error) if callback and error - _writeDefaultsSecure: (newDefaults, callback) -> - @_readDefaultsSecure (currentDefaults, entireFileJSON) => - entireFileJSON['LSHandlers'] = newDefaults - data = JSON.stringify(entireFileJSON) - tmpPath = "#{launchServicesPlistPath}.json" - fs.writeFile tmpPath, data, (err) => - return callback(err) if callback and err - exec "plutil -convert binary1 \"#{tmpPath}\" -o \"#{launchServicesPlistPath}\"", => - fs.unlink(tmpPath) - @triggerSystemReload(callback) - - _writeDefaultsPreYosemite: (defaults, callback) -> - # Convert the defaults JSON back into Apple's json-like - # format. (I think it predates JSON?) - json = JSON.stringify(defaults) - plist = json.replace(/\[/g, '(') - plist = plist.replace(/\]/g, ')') - regex = /\"([a-zA-Z^"]*)\":\"([^"]*)\",?/ - while (match = regex.exec(plist)) != null - [text, key, val] = match - plist = plist.replace(text, "#{key} = \"#{val}\";") - - # Write the new defaults back to the system - exec "defaults write ~/Library/Preferences/com.apple.LaunchServices.plist LSHandlers '#{plist}'", (err, stdout, stderr) => - return callback(err) if callback and err - @triggerSystemReload(callback) - - triggerSystemReload: (callback) -> - exec "/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister -kill -r -domain local -domain system -domain user", (err, stdout, stderr) -> - callback(err) if callback + exec "plutil -convert binary1 \"#{tmpPath}\" -o \"#{plistPath}\"", -> + fs.unlink(tmpPath) + exec "/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister -kill -r -domain local -domain system -domain user", (err, stdout, stderr) -> + callback(err) if callback isRegisteredForURLScheme: (scheme, callback) -> throw new Error "isRegisteredForURLScheme is async, provide a callback" unless callback diff --git a/internal_packages/notification-mailto/spec/launch-services-spec.coffee b/internal_packages/notification-mailto/spec/launch-services-spec.coffee index 8e6f76933..3dae488b2 100644 --- a/internal_packages/notification-mailto/spec/launch-services-spec.coffee +++ b/internal_packages/notification-mailto/spec/launch-services-spec.coffee @@ -2,19 +2,27 @@ _ = require 'underscore-plus' proxyquire = require 'proxyquire' stubDefaultsJSON = null -stubDefaults = null execHitory = [] ChildProcess = exec: (command, callback) -> + console.log(command) execHitory.push(arguments) - if command is "defaults read com.apple.launchservices LSHandlers" - callback(null, stubDefaults, null) - else - callback(null, '', null) + callback(null, '', null) + +fs = + exists: (path, callback) -> + callback(true) + readFile: (path, callback) -> + callback(null, JSON.stringify(stubDefaultsJSON)) + readFileSync: (path) -> + JSON.stringify(stubDefaultsJSON) + writeFileSync: (path) -> + null LaunchServices = proxyquire "../lib/launch-services", - "child_process": ChildProcess + "child_process": ChildProcess + "fs": fs describe "LaunchServices", -> beforeEach -> @@ -109,99 +117,7 @@ describe "LaunchServices", -> LSHandlerURLScheme: 'mailto' } ] - stubDefaults = """ - ( - { - LSHandlerRoleAll = "com.apple.dt.xcode"; - LSHandlerURLScheme = xcdoc; - }, - { - LSHandlerRoleAll = "com.fournova.tower"; - LSHandlerURLScheme = "github-mac"; - }, - { - LSHandlerRoleAll = "com.fournova.tower"; - LSHandlerURLScheme = sourcetree; - }, - { - LSHandlerRoleAll = "com.google.chrome"; - LSHandlerURLScheme = http; - }, - { - LSHandlerRoleAll = "com.google.chrome"; - LSHandlerURLScheme = https; - }, - { - LSHandlerContentType = "public.html"; - LSHandlerRoleViewer = "com.google.chrome"; - }, - { - LSHandlerContentType = "public.url"; - LSHandlerRoleViewer = "com.google.chrome"; - }, - { - LSHandlerContentType = "com.apple.ical.backup"; - LSHandlerRoleAll = "com.apple.ical"; - }, - { - LSHandlerContentTag = icalevent; - LSHandlerContentTagClass = "public.filename-extension"; - LSHandlerRoleAll = "com.apple.ical"; - }, - { - LSHandlerContentTag = icaltodo; - LSHandlerContentTagClass = "public.filename-extension"; - LSHandlerRoleAll = "com.apple.reminders"; - }, - { - LSHandlerRoleAll = "com.apple.ical"; - LSHandlerURLScheme = webcal; - }, - { - LSHandlerContentTag = coffee; - LSHandlerContentTagClass = "public.filename-extension"; - LSHandlerRoleAll = "com.sublimetext.2"; - }, - { - LSHandlerRoleAll = "com.apple.facetime"; - LSHandlerURLScheme = facetime; - }, - { - LSHandlerRoleAll = "com.apple.dt.xcode"; - LSHandlerURLScheme = xcdevice; - }, - { - LSHandlerContentType = "public.png"; - LSHandlerRoleAll = "com.macromedia.fireworks"; - }, - { - LSHandlerRoleAll = "com.apple.dt.xcode"; - LSHandlerURLScheme = xcbot; - }, - { - LSHandlerRoleAll = "com.microsoft.rdc.mac"; - LSHandlerURLScheme = rdp; - }, - { - LSHandlerContentTag = rdp; - LSHandlerContentTagClass = "public.filename-extension"; - LSHandlerRoleAll = "com.microsoft.rdc.mac"; - }, - { - LSHandlerContentType = "public.json"; - LSHandlerRoleAll = "com.sublimetext.2"; - }, - { - LSHandlerContentTag = cson; - LSHandlerContentTagClass = "public.filename-extension"; - LSHandlerRoleAll = "com.sublimetext.2"; - }, - { - LSHandlerRoleAll = "com.apple.mail"; - LSHandlerURLScheme = mailto; - } - ) - """ + describe "when the platform is darwin", -> beforeEach -> @@ -213,118 +129,90 @@ describe "LaunchServices", -> it "should return true", -> expect(@services.available()).toEqual(true) - describe "pre-Yosemite", -> - beforeEach -> - @services.isYosemiteOrGreater = (callback) -> callback(false) + describe "readDefaults", -> - describe "readDefaults", -> - it "should return the user defaults registered with the system via `defaults`", -> - response = null - runs -> - @services.readDefaults (defaults) -> - response = defaults - waitsFor -> - response - runs -> - expect(response).toEqual(stubDefaultsJSON) + describe "writeDefaults", -> + it "should `lsregister` to reload defaults after saving them", -> + callback = jasmine.createSpy('callback') + @services.writeDefaults(stubDefaultsJSON, callback) + callback.callCount is 1 + command = execHitory[2][0] + expect(command).toBe("/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister -kill -r -domain local -domain system -domain user") - describe "writeDefaults", -> - it "should covert the defaults to the plist format and call `defaults write`", -> - callback = jasmine.createSpy('callback') - @services.writeDefaults(stubDefaultsJSON, callback) - command = execHitory[0][0] - expect(command).toBe("""defaults write ~/Library/Preferences/com.apple.LaunchServices.plist LSHandlers '({LSHandlerRoleAll = "com.apple.dt.xcode";LSHandlerURLScheme = "xcdoc";},{LSHandlerRoleAll = "com.fournova.tower";LSHandlerURLScheme = "github-mac";},{LSHandlerRoleAll = "com.fournova.tower";LSHandlerURLScheme = "sourcetree";},{LSHandlerRoleAll = "com.google.chrome";LSHandlerURLScheme = "http";},{LSHandlerRoleAll = "com.google.chrome";LSHandlerURLScheme = "https";},{LSHandlerContentType = "public.html";LSHandlerRoleViewer = "com.google.chrome";},{LSHandlerContentType = "public.url";LSHandlerRoleViewer = "com.google.chrome";},{LSHandlerContentType = "com.apple.ical.backup";LSHandlerRoleAll = "com.apple.ical";},{LSHandlerContentTag = "icalevent";LSHandlerContentTagClass = "public.filename-extension";LSHandlerRoleAll = "com.apple.ical";},{LSHandlerContentTag = "icaltodo";LSHandlerContentTagClass = "public.filename-extension";LSHandlerRoleAll = "com.apple.reminders";},{LSHandlerRoleAll = "com.apple.ical";LSHandlerURLScheme = "webcal";},{LSHandlerContentTag = "coffee";LSHandlerContentTagClass = "public.filename-extension";LSHandlerRoleAll = "com.sublimetext.2";},{LSHandlerRoleAll = "com.apple.facetime";LSHandlerURLScheme = "facetime";},{LSHandlerRoleAll = "com.apple.dt.xcode";LSHandlerURLScheme = "xcdevice";},{LSHandlerContentType = "public.png";LSHandlerRoleAll = "com.macromedia.fireworks";},{LSHandlerRoleAll = "com.apple.dt.xcode";LSHandlerURLScheme = "xcbot";},{LSHandlerRoleAll = "com.microsoft.rdc.mac";LSHandlerURLScheme = "rdp";},{LSHandlerContentTag = "rdp";LSHandlerContentTagClass = "public.filename-extension";LSHandlerRoleAll = "com.microsoft.rdc.mac";},{LSHandlerContentType = "public.json";LSHandlerRoleAll = "com.sublimetext.2";},{LSHandlerContentTag = "cson";LSHandlerContentTagClass = "public.filename-extension";LSHandlerRoleAll = "com.sublimetext.2";},{LSHandlerRoleAll = "com.apple.mail";LSHandlerURLScheme = "mailto";})'""") + describe "isRegisteredForURLScheme", -> + it "should require a callback is provided", -> + expect( -> @services.isRegisteredForURLScheme('mailto')).toThrow() - it "should `lsregister` to reload defaults after saving them", -> - callback = jasmine.createSpy('callback') - @services.writeDefaults(stubDefaultsJSON, callback) - command = execHitory[1][0] - expect(command).toBe("/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister -kill -r -domain local -domain system -domain user") + it "should return true if a matching `LSHandlerURLScheme` record exists for the bundle identifier", -> + spyOn(@services, 'readDefaults').andCallFake (callback) -> + callback([{ + "LSHandlerRoleAll": "com.apple.dt.xcode", + "LSHandlerURLScheme": "xcdoc" + }, { + "LSHandlerContentTag": "cson", + "LSHandlerContentTagClass": "public.filename-extension", + "LSHandlerRoleAll": "com.sublimetext.2" + }, { + "LSHandlerRoleAll": "com.inbox.edgehill", + "LSHandlerURLScheme": "mailto" + }]) + @services.isRegisteredForURLScheme 'mailto', (registered) -> + expect(registered).toBe(true) - describe "isRegisteredForURLScheme", -> - it "should require a callback is provided", -> - expect( -> @services.isRegisteredForURLScheme('mailto')).toThrow() + it "should return false when other records exist for the bundle identifier but do not match", -> + spyOn(@services, 'readDefaults').andCallFake (callback) -> + callback([{ + LSHandlerRoleAll: "com.apple.dt.xcode", + LSHandlerURLScheme: "xcdoc" + },{ + LSHandlerContentTag: "cson", + LSHandlerContentTagClass: "public.filename-extension", + LSHandlerRoleAll: "com.sublimetext.2" + },{ + LSHandlerRoleAll: "com.inbox.edgehill", + LSHandlerURLScheme: "atom" + }]) + @services.isRegisteredForURLScheme 'mailto', (registered) -> + expect(registered).toBe(false) - it "should return true if a matching `LSHandlerURLScheme` record exists for the bundle identifier", -> - stubDefaults = """ - ( - { - LSHandlerRoleAll = "com.apple.dt.xcode"; - LSHandlerURLScheme = xcdoc; - }, - { - LSHandlerContentTag = cson; - LSHandlerContentTagClass = "public.filename-extension"; - LSHandlerRoleAll = "com.sublimetext.2"; - }, - { - LSHandlerRoleAll = "com.inbox.edgehill"; - LSHandlerURLScheme = mailto; - } - ) - """ - @services.isRegisteredForURLScheme 'mailto', (registered) -> - expect(registered).toBe(true) + it "should return false if another bundle identifier is registered for the `LSHandlerURLScheme`", -> + spyOn(@services, 'readDefaults').andCallFake (callback) -> + callback([{ + LSHandlerRoleAll: "com.apple.dt.xcode", + LSHandlerURLScheme: "xcdoc" + },{ + LSHandlerContentTag: "cson", + LSHandlerContentTagClass: "public.filename-extension", + LSHandlerRoleAll: "com.sublimetext.2" + },{ + LSHandlerRoleAll: "com.apple.mail", + LSHandlerURLScheme: "mailto" + }]) + @services.isRegisteredForURLScheme 'mailto', (registered) -> + expect(registered).toBe(false) - it "should return false when other records exist for the bundle identifier but do not match", -> - stubDefaults = """ - ( - { - LSHandlerRoleAll = "com.apple.dt.xcode"; - LSHandlerURLScheme = xcdoc; - }, - { - LSHandlerContentTag = cson; - LSHandlerContentTagClass = "public.filename-extension"; - LSHandlerRoleAll = "com.sublimetext.2"; - }, - { - LSHandlerRoleAll = "com.inbox.edgehill"; - LSHandlerURLScheme = atom; - } - ) - """ - @services.isRegisteredForURLScheme 'mailto', (registered) -> - expect(registered).toBe(false) + describe "registerForURLScheme", -> + it "should remove any existing records for the `LSHandlerURLScheme`", -> + @services.registerForURLScheme 'mailto', => + @services.readDefaults (values) -> + expect(JSON.stringify(values).indexOf('com.apple.mail')).toBe(-1) - it "should return false if another bundle identifier is registered for the `LSHandlerURLScheme`", -> - stubDefaults = """ - ( - { - LSHandlerRoleAll = "com.apple.dt.xcode"; - LSHandlerURLScheme = xcdoc; - }, - { - LSHandlerContentTag = cson; - LSHandlerContentTagClass = "public.filename-extension"; - LSHandlerRoleAll = "com.sublimetext.2"; - }, - { - LSHandlerRoleAll = "com.apple.mail"; - LSHandlerURLScheme = mailto; - } - ) - """ - @services.isRegisteredForURLScheme 'mailto', (registered) -> - expect(registered).toBe(false) + it "should add a record for the `LSHandlerURLScheme` and the app's bundle identifier", -> + @services.registerForURLScheme 'mailto', => + @services.readDefaults (defaults) -> + match = _.find defaults, (d) -> + d.LSHandlerURLScheme is 'mailto' and d.LSHandlerRoleAll is 'com.inbox.edgehill' + expect(match).not.toBe(null) - describe "registerForURLScheme", -> - it "should remove any existing records for the `LSHandlerURLScheme`", -> - @services.registerForURLScheme 'mailto', => - @services.readDefaults (values) -> - expect(JSON.stringify(values).indexOf('com.apple.mail')).toBe(-1) - - it "should add a record for the `LSHandlerURLScheme` and the app's bundle identifier", -> - @services.registerForURLScheme 'mailto', => - @services.readDefaults (defaults) -> - match = _.find defaults, (d) -> - d.LSHandlerURLScheme is 'mailto' and d.LSHandlerRoleAll is 'com.inbox.edgehill' - expect(match).not.toBe(null) - - it "should write the new defaults", -> - spyOn(@services, 'writeDefaults') - @services.registerForURLScheme('mailto') - expect(@services.writeDefaults).toHaveBeenCalled() + it "should write the new defaults", -> + spyOn(@services, 'readDefaults').andCallFake (callback) -> + callback([{ + LSHandlerRoleAll: "com.apple.dt.xcode", + LSHandlerURLScheme: "xcdoc" + }]) + spyOn(@services, 'writeDefaults') + @services.registerForURLScheme('mailto') + expect(@services.writeDefaults).toHaveBeenCalled() describe "on other platforms", -> describe "available", ->