-
Notifications
You must be signed in to change notification settings - Fork 10
/
msda.py
executable file
·431 lines (354 loc) · 10.7 KB
/
msda.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
#!/usr/bin/python
"""\
Directly modifies launchservices plists to set default file associations in macOS.
A somewhat complete list of UTI's can be found here:
https://escapetech.eu/manuals/qdrop/uti.html\
"""
from __future__ import print_function
import os, shutil, subprocess, sys
from argparse import ArgumentParser, RawDescriptionHelpFormatter
from platform import mac_ver
from plistlib import readPlist, writePlist
from tempfile import NamedTemporaryFile
###############################################################################
#
# User-Editable Settings
#
###############################################################################
JAMF = False # Is this being used as a Jamf script?
TMP_PREFIX = 'msda_tmp_' # Prefixes tempoaray files created by this app
USER_HOMES_LOCATION = '/Users' # Where users' home directories are located
###############################################################################
#
# App Information
#
###############################################################################
__author__ = 'David G. Rosenberg'
__copyright__ = 'Copyright (c), Mac Set Default Apps'
__license__ = 'MIT'
__version__ = '1.1.3'
__email__ = 'dgrosenberg@icloud.com'
###############################################################################
#
# Settings Users Shouldn't Change
#
###############################################################################
LSREGISTER_BINARY = '/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/Support/lsregister'
OS_VERSION = float(mac_ver()[0][3:])
PLIST_NAME = 'com.apple.launchservices.secure.plist'
PLIST_RELATIVE_LOCATION = 'Library/Preferences/com.apple.LaunchServices/'
USER_TEMPLATE_LOCATION = '/Library/User Template/English.lproj'
if OS_VERSION < 15.0:
USER_TEMPLATE_LOCATION = os.path.join(
'/System', USER_TEMPLATE_LOCATION
)
###############################################################################
#
# Functions
#
###############################################################################
def create_plist_parents(plist_path):
"""
Creates the directory structure if the provided plist doesn't exist
"""
# if the specified plist already exists, don't do anything
if os.path.isfile(plist_path):
return False
# if the specified plist's parent directories already exist, don't do
# anything
parent_path = os.path.dirname(plist_path)
if os.path.exists(parent_path):
return False
# create the parent directories for the specified plist
os.makedirs(parent_path)
return plist_path
def create_user_ls_path(username):
path = os.path.join(
USER_HOMES_LOCATION,
username,
PLIST_RELATIVE_LOCATION,
PLIST_NAME,
)
return path
def create_template_ls_path():
path = os.path.join(
USER_TEMPLATE_LOCATION,
PLIST_RELATIVE_LOCATION,
PLIST_NAME,
)
return path
def get_current_username():
who_cmd = subprocess.Popen(
('who'),
stdout=subprocess.PIPE,
)
grep_cmd = subprocess.Popen(
('grep', 'console'),
stdin=who_cmd.stdout, stdout=subprocess.PIPE,
)
username = subprocess.check_output(
('cut', '-d', ' ', '-f1'),
stdin=grep_cmd.stdout,
)
return username.strip()
def gather_user_ls_paths():
gathered_users = os.listdir(USER_HOMES_LOCATION)
ls_paths = []
for user in gathered_users:
user_ls_path = create_user_ls_path(user)
if os.path.exists(os.path.dirname(user_ls_path)):
ls_paths.append(user_ls_path)
return ls_paths
###############################################################################
#
# Class Definitions
#
###############################################################################
class LSHandler(object):
def _from_dict(self, from_dict):
"""
Creates an LSHandler object from a dictionary
"""
# grab the role from the string containing it
self.role = from_dict['LSHandlerPreferredVersions'].keys()[0]
self.role = self.role[13:].lower()
# grab the UTI/protocol
try:
# for if it's a UTI
self.uti = from_dict['LSHandlerContentType'].lower()
self._type = 'ContentType'
except KeyError:
# for if it's a protocol
self.uti = from_dict['LSHandlerURLScheme'].lower()
self._type = 'URLScheme'
# grab the App ID
self.app_id = from_dict[self._role_key].lower()
def _from_properties(self, **kwargs):
"""
Creates an LSHandler from specified properties
"""
self.app_id = kwargs['app_id'].lower()
self.uti = kwargs['uti'].lower()
# determines if we're working with a UTI or protocol based on the
# presence of periods
if '.' in self.uti:
self._type = 'ContentType'
self.role = kwargs.get('role') or 'all'
self.role = self.role.lower()
else:
self._type = 'URLScheme'
self.role = 'all'
def __init__(self, from_dict=None, **kwargs):
if from_dict:
self._from_dict(from_dict)
else:
self._from_properties(**kwargs)
@property
def _role_key(self):
"""
Generates the dictionary key for an LSHandler's role
"""
return 'LSHandlerRole' + self.role.capitalize()
def __eq__(self, other):
"""
Two LSHandlers for the same role and UTI would be considered equal
"""
compare_utis = self.uti == other.uti
if self.role == 'all' or other.role == 'all':
compare_roles = True
else:
compare_roles = self.role == other.role
return compare_utis and compare_roles
def __ne__(self, other):
"""
Inverts the __eq__ function
"""
return not self == other
def __iter__(self):
yield ('LSHandler' + self._type, self.uti)
yield (self._role_key, self.app_id)
yield ('LSHandlerPreferredVersions', { self._role_key: '-' })
class LaunchServices(object):
def __init__(self, plist=None):
self.handlers = []
self.plist = plist
if self.plist:
self.read()
def __iter__(self):
yield ('LSHandlers', [ dict(h) for h in self.handlers ])
def read(self):
"""
Reads the plist at the specified path into a LaunchServices object,
creating LSHandler objects as necesary
"""
# is the specified plist doesn't exist, there's nothing to read
if not os.path.isfile(self.plist):
return
with NamedTemporaryFile(prefix=TMP_PREFIX, delete=True) as tmp_plist:
# copy the target plist to a temporary file
tmp_path = tmp_plist.name
shutil.copyfile(self.plist, tmp_path)
# convert to XML from binary
convert_command = '/usr/bin/plutil -convert xml1 ' + tmp_path
subprocess.check_output(convert_command.split())
# read the plist
plist = readPlist(tmp_path)
# convert any specified LSHandlers to objects
for lshandler in plist['LSHandlers']:
self.handlers.append(LSHandler(from_dict=lshandler))
def write(self, plist=None):
"""
Writes this object to the specified plist, formatting it as a
LaunchServices plist
"""
# allow for alternate destinations (mostly for testing)
if not plist:
plist = self.plist
# create parent directories if they don't exist
create_plist_parents(plist)
with NamedTemporaryFile(prefix=TMP_PREFIX, delete=True) as tmp_plist:
# write the LaunchServices object to a temporary file
tmp_path = tmp_plist.name
writePlist(dict(self), tmp_path)
# convert it to binary
convert_command = '/usr/bin/plutil -convert binary1 ' + tmp_path
subprocess.check_output(convert_command.split())
# and overwrite the specified plist
shutil.copyfile(tmp_path, plist)
@property
def app_ids(self):
"""
Provides a set of all App IDs set as default handlers
"""
collected_app_ids = [ h.app_id for h in self.handlers ]
return set(collected_app_ids)
def set_handler(self, lshandler=None, **kwargs):
"""
Adds the provided LSHandler to the LaunchServices object, converting
to a new LSHandler if necesary
"""
if not lshandler:
new_lshandler = LSHandler(**kwargs)
else:
new_lshandler = lshandler
self.handlers = [ h for h in self.handlers if h != new_lshandler ]
self.handlers.append(new_lshandler)
return new_lshandler
###############################################################################
#
# Main Functions
#
###############################################################################
def set_command(args):
# print('Setting "{}" as a default handler in...'.format(args.app_id))
# Check for current user
current_username = get_current_username()
# Collect plists
plists = []
if args.feu:
plists.extend(gather_user_ls_paths())
elif current_username != '':
plists.append(create_user_ls_path(current_username))
if args.fut:
plists.append(create_template_ls_path())
# Process plists
for plist in plists:
ls = LaunchServices(plist)
# print(' "{}"...'.format(ls.plist))
# Combine submitted UTIs and protocols
if not args.uti:
args.uti = []
if args.protocol:
args.uti += [ [p, None] for p in args.protocol ]
# Create and set handlers
for uti in args.uti:
# if uti[1] != None:
# print(' for "{}" with role "{}"'.format(uti[0], uti[1]))
# else:
# print(' for "{}" with role "all"'.format(uti[0]))
ls.set_handler(
app_id=args.app_id,
uti=uti[0],
role=uti[1],
)
ls.write()
return 0
def main(arguments=None):
if JAMF:
# Strip first 3 args and convert 4th to a list
arguments = arguments[3].split()
# Global parser setup
parser = ArgumentParser(
description=__doc__,
formatter_class=RawDescriptionHelpFormatter,
epilog='Please email {} with any issues'.format(
__email__,
)
)
# parser.add_argument(
# '-v', '--verbose',
# help='verbose output',
# action='store_true',
# )
parser.add_argument(
'--version',
help='prints the current version',
action='version',
version=__version__,
)
subparsers = parser.add_subparsers(
help='the subcommand to run',
metavar='command',
dest='command',
)
# "set" parser setup
set_parser = subparsers.add_parser(
'set',
help='set LSHandlers for a given App ID',
)
set_parser.set_defaults(func=set_command)
set_parser.add_argument(
'app_id',
help='the identifier of the application to set as a default',
type=str,
)
set_parser.add_argument(
'-feu',
help='updates all existing users\' launch services',
action='store_true',
)
set_parser.add_argument(
'-fut',
help='updates the user template\'s launch services',
action='store_true',
)
set_parser.add_argument(
'-p', '--protocol',
help='protocols to associate with the given app ID',
action='append',
)
set_parser.add_argument(
'-u', '--uti',
help='UTIs and roles to associate with the given app ID',
action='append',
nargs=2,
metavar=('UTI', 'ROLE'),
)
# Process specified args
args = parser.parse_args(arguments)
# global verbose
# verbose = args.verbose
# print('')
# Run specified function with processed args
return args.func(args)
if __name__ == '__main__':
exit_code = main(sys.argv[1:])
# Rebuild launch services
rebuild_command = [LSREGISTER_BINARY,
'-kill', '-r',
'-domain', 'local',
'-domain', '-system',
'-domain', '-user',
]
subprocess.check_output(rebuild_command)
sys.exit(exit_code)