From 84e68946c69f2d5799ef0b77a0c26817d0a765c9 Mon Sep 17 00:00:00 2001 From: Filipp Lepalaan Date: Mon, 7 Nov 2016 18:53:55 +0200 Subject: Renamed to maucl --- README.md | 8 +-- maucl.py | 190 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ mowgli.py | 190 -------------------------------------------------------------- tests.py | 6 +- 4 files changed, 197 insertions(+), 197 deletions(-) create mode 100755 maucl.py delete mode 100755 mowgli.py diff --git a/README.md b/README.md index 4df377b..6170c2c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ ### Introduction -mowgli (from MAUCLI (Microsoft AutoUpdate Command Line Interface)) is a command-line tool for installing Office 2016 updates. +maucl (Microsoft AutoUpdate Command Line) is a command-line tool for installing Office 2016 updates. ### System Requirements @@ -12,15 +12,15 @@ mowgli (from MAUCLI (Microsoft AutoUpdate Command Line Interface)) is a command- To list all available updates: -```mowgli.py -l``` +```maucl.py -l``` To update all installed Office applications, just issue: -```sudo mowgli.py -ia``` +```sudo maucl.py -ia``` You can also supply an optional path to the home folder: -```sudo mowgli.py -ia /Users/shared``` +```sudo maucl.py -ia /Users/shared``` Special thanks to [Charles](https://www.charlesproxy.com) - the excellent web debugging proxy for making the reverse-engineering possible! diff --git a/maucl.py b/maucl.py new file mode 100755 index 0000000..62d5b42 --- /dev/null +++ b/maucl.py @@ -0,0 +1,190 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""Microsoft AutoUpdate Command Line Utility.""" + +import os +import sys +import httplib +import plistlib +import tempfile +import subprocess + + +UUID = 'C1297A47-86C4-4C1F-97FA-950631F94777' +NAMES = { + 'XCEL15': 'Excel', + 'ONMC15': 'OneNote', + 'OPIM15': 'Outlook', + 'PPT315': 'PowerPoint', + 'MSWD15': 'Word', + 'MSau03': 'AutoUpdate', + 'SLVT': 'Silverlight', +} + + +BASEURL = '/pr/%s/OfficeMac/' % UUID +PREF_PATH = 'Library/Preferences/com.microsoft.autoupdate2.plist' + + +def get_plist(path): + """Return plist dict regardless of format.""" + plist = subprocess.check_output(['/usr/bin/plutil', + '-convert', 'xml1', + path, '-o', '-']) + return plistlib.readPlistFromString(plist) + + +def set_pref(k='HowToCheck', v='Manual'): + """Set AU pref k to v.""" + subprocess.call(['/usr/bin/defaults', + 'write', + 'com.microsoft.autoupdate2', + k, + v]) + + +def check(home=None, pref=PREF_PATH): + """Collect info about available updates.""" + results = [] + + if home is None: + home = os.getenv('HOME') + + pref = os.path.join(home, pref) + + try: + plist = get_plist(os.path.expanduser(pref)) + except Exception as e: + raise Exception('Failed to read MAU prefs: %s' % e) + + apps = plist.get('Applications') + + for a in apps.items(): + path, info = a + app_id = info.get('Application ID') + + # Skip Microsoft Error Reporting + if app_id == 'Merp2': + continue + + try: + app_info = get_plist(os.path.join(path, 'Contents/Info.plist')) + except Exception as e: + continue + + result = {'id': app_id, 'installed': app_info.get('CFBundleVersion')} + result['name'] = NAMES.get(app_id, app_id) + result['lcid'] = info.get('LCID') + + # Lync (UCCP14) is special + filename = '0409%s.xml' if app_id == 'UCCP14' else '0409%s-chk.xml' + + try: + conn = httplib.HTTPSConnection('officecdn.microsoft.com') + conn.request("GET", BASEURL + filename % app_id) + data = conn.getresponse().read() + except Exception as e: + raise Exception('Failed to check for updates: %s' % e) + + try: + p = plistlib.readPlistFromString(data) + + if app_id == 'UCCP14': # Lync being special again + p = p[0] + result['location'] = p.get('Location') + versions = p['Triggers']['Lync']['Versions'] + result['needs_update'] = result['installed'] in versions + else: + result['date'] = p.get('Date') + result['type'] = p.get('Type') + result['available'] = p.get('Update Version') + result['needs_update'] = result['installed'] != result['available'] + + if result['needs_update']: + url = BASEURL + filename % app_id + conn.request("GET", url.replace('-chk', '')) + data = conn.getresponse().read() + # Fetch the update details + updates = plistlib.readPlistFromString(data) + for i in updates: + if i.get('Baseline Version') == result['installed'] or app_id == 'MSau03': + result['location'] = i.get('Location') + result['size'] = i.get('FullUpdaterSize') + results.append(result) + except Exception as e: + print('Failed to check %s' % app_id, e) + + finally: + conn.close() + + return results + + +def download(url): + """Download url to temporary folder.""" + temp = os.path.join(tempfile.gettempdir(), 'mowgli') + + if not os.path.exists(temp): + os.mkdir(temp) + + fn = url.split('/')[-1] + fp = os.path.join(temp, fn) + + if not os.path.exists(fp): + subprocess.call(['/usr/bin/curl', url, '-o', fp]) + + return fp + + +def install(pkg): + """Install a package.""" + r = subprocess.call(['/usr/sbin/installer', '-pkg', pkg, '-target', '/']) + if r > 0: + raise Exception('Failed to install package %s' % pkg) + + os.remove(pkg) + + +if __name__ == '__main__': + + if len(sys.argv) < 2: + print("usage: {0} [-l | -i [home]".format(os.path.basename(sys.argv[0]))) + sys.exit(1) + + if sys.argv[1] == 'enable': + set_pref('HowToCheck', 'Manual') + print('* Automatic updates enabled') + sys.exit(0) + + if sys.argv[1] == 'disable': + set_pref('HowToCheck', 'Automatic') + print('* Automatic updates disabled') + sys.exit(0) + + print("* Finding available software") + + if len(sys.argv) > 2: + if not os.path.exists(sys.argv[2]): + raise Exception('Invalid MAU pref file %s' % sys.argv[2]) + updates = check(sys.argv[2]) + else: + updates = check() + + updates = [u for u in updates if u['needs_update']] + + if sys.argv[1] == '-l': + for u in updates: + print(" * {id}\n {name} {installed} - {available} [{type}]".format(**u)) + + if sys.argv[1] == '-ia': + for u in updates: + print('* Downloading %s %s' % (u['name'], u['available'])) + pkg = download(u['location']) + print('* Installing %s' % pkg) + install(pkg) + + if len(updates) < 1: + print("* No updates available") + sys.exit(1) + + sys.exit(0) diff --git a/mowgli.py b/mowgli.py deleted file mode 100755 index 62d5b42..0000000 --- a/mowgli.py +++ /dev/null @@ -1,190 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -"""Microsoft AutoUpdate Command Line Utility.""" - -import os -import sys -import httplib -import plistlib -import tempfile -import subprocess - - -UUID = 'C1297A47-86C4-4C1F-97FA-950631F94777' -NAMES = { - 'XCEL15': 'Excel', - 'ONMC15': 'OneNote', - 'OPIM15': 'Outlook', - 'PPT315': 'PowerPoint', - 'MSWD15': 'Word', - 'MSau03': 'AutoUpdate', - 'SLVT': 'Silverlight', -} - - -BASEURL = '/pr/%s/OfficeMac/' % UUID -PREF_PATH = 'Library/Preferences/com.microsoft.autoupdate2.plist' - - -def get_plist(path): - """Return plist dict regardless of format.""" - plist = subprocess.check_output(['/usr/bin/plutil', - '-convert', 'xml1', - path, '-o', '-']) - return plistlib.readPlistFromString(plist) - - -def set_pref(k='HowToCheck', v='Manual'): - """Set AU pref k to v.""" - subprocess.call(['/usr/bin/defaults', - 'write', - 'com.microsoft.autoupdate2', - k, - v]) - - -def check(home=None, pref=PREF_PATH): - """Collect info about available updates.""" - results = [] - - if home is None: - home = os.getenv('HOME') - - pref = os.path.join(home, pref) - - try: - plist = get_plist(os.path.expanduser(pref)) - except Exception as e: - raise Exception('Failed to read MAU prefs: %s' % e) - - apps = plist.get('Applications') - - for a in apps.items(): - path, info = a - app_id = info.get('Application ID') - - # Skip Microsoft Error Reporting - if app_id == 'Merp2': - continue - - try: - app_info = get_plist(os.path.join(path, 'Contents/Info.plist')) - except Exception as e: - continue - - result = {'id': app_id, 'installed': app_info.get('CFBundleVersion')} - result['name'] = NAMES.get(app_id, app_id) - result['lcid'] = info.get('LCID') - - # Lync (UCCP14) is special - filename = '0409%s.xml' if app_id == 'UCCP14' else '0409%s-chk.xml' - - try: - conn = httplib.HTTPSConnection('officecdn.microsoft.com') - conn.request("GET", BASEURL + filename % app_id) - data = conn.getresponse().read() - except Exception as e: - raise Exception('Failed to check for updates: %s' % e) - - try: - p = plistlib.readPlistFromString(data) - - if app_id == 'UCCP14': # Lync being special again - p = p[0] - result['location'] = p.get('Location') - versions = p['Triggers']['Lync']['Versions'] - result['needs_update'] = result['installed'] in versions - else: - result['date'] = p.get('Date') - result['type'] = p.get('Type') - result['available'] = p.get('Update Version') - result['needs_update'] = result['installed'] != result['available'] - - if result['needs_update']: - url = BASEURL + filename % app_id - conn.request("GET", url.replace('-chk', '')) - data = conn.getresponse().read() - # Fetch the update details - updates = plistlib.readPlistFromString(data) - for i in updates: - if i.get('Baseline Version') == result['installed'] or app_id == 'MSau03': - result['location'] = i.get('Location') - result['size'] = i.get('FullUpdaterSize') - results.append(result) - except Exception as e: - print('Failed to check %s' % app_id, e) - - finally: - conn.close() - - return results - - -def download(url): - """Download url to temporary folder.""" - temp = os.path.join(tempfile.gettempdir(), 'mowgli') - - if not os.path.exists(temp): - os.mkdir(temp) - - fn = url.split('/')[-1] - fp = os.path.join(temp, fn) - - if not os.path.exists(fp): - subprocess.call(['/usr/bin/curl', url, '-o', fp]) - - return fp - - -def install(pkg): - """Install a package.""" - r = subprocess.call(['/usr/sbin/installer', '-pkg', pkg, '-target', '/']) - if r > 0: - raise Exception('Failed to install package %s' % pkg) - - os.remove(pkg) - - -if __name__ == '__main__': - - if len(sys.argv) < 2: - print("usage: {0} [-l | -i [home]".format(os.path.basename(sys.argv[0]))) - sys.exit(1) - - if sys.argv[1] == 'enable': - set_pref('HowToCheck', 'Manual') - print('* Automatic updates enabled') - sys.exit(0) - - if sys.argv[1] == 'disable': - set_pref('HowToCheck', 'Automatic') - print('* Automatic updates disabled') - sys.exit(0) - - print("* Finding available software") - - if len(sys.argv) > 2: - if not os.path.exists(sys.argv[2]): - raise Exception('Invalid MAU pref file %s' % sys.argv[2]) - updates = check(sys.argv[2]) - else: - updates = check() - - updates = [u for u in updates if u['needs_update']] - - if sys.argv[1] == '-l': - for u in updates: - print(" * {id}\n {name} {installed} - {available} [{type}]".format(**u)) - - if sys.argv[1] == '-ia': - for u in updates: - print('* Downloading %s %s' % (u['name'], u['available'])) - pkg = download(u['location']) - print('* Installing %s' % pkg) - install(pkg) - - if len(updates) < 1: - print("* No updates available") - sys.exit(1) - - sys.exit(0) diff --git a/tests.py b/tests.py index b67a966..c673dd2 100755 --- a/tests.py +++ b/tests.py @@ -4,12 +4,12 @@ import unittest import subprocess -import mowgli +import maucl class DefaultTestCase(unittest.TestCase): def test_disable_au(self): - mowgli.set_pref() + maucl.set_pref() o = subprocess.check_output(['defaults', 'read', 'com.microsoft.autoupdate2', @@ -17,7 +17,7 @@ class DefaultTestCase(unittest.TestCase): self.assertEquals(o.strip(), 'Manual') def test_enable_au(self): - mowgli.set_pref(v='Automatic') + maucl.set_pref(v='Automatic') o = subprocess.check_output(['defaults', 'read', 'com.microsoft.autoupdate2', -- cgit v1.2.3