diff options
-rw-r--r-- | README.md | 55 | ||||
-rwxr-xr-x | mowgli.py | 155 |
2 files changed, 210 insertions, 0 deletions
diff --git a/README.md b/README.md new file mode 100644 index 0000000..bd48d07 --- /dev/null +++ b/README.md @@ -0,0 +1,55 @@ +### Introduction + +mowgli (from MAUCLI (Microsoft AutoUpdate Command Line Interface)) is a command-line tool for installing Office 2016 updates. + + +### System Requirements + +- Microsoft Office for Mac 2016 + + +### Usage + +To list all available updates: + +```mowgli.py -l``` + +To update all installed Office applications, just issue: + +```sudo mowgli.py -ia``` + +You can also supply an optional path to the MAU pref file: + +```sudo mowgli.py -ia /Users/shared/Library/Preferences/com.microsoft.autoupdate2.plist``` + + +Special thanks to [Charles](https://www.charlesproxy.com) - the excellent web debugging proxy for making the reverse-engineering possible! + + + +### License + +Copyright (c) 2016, Filipp Lepalaan All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +
\ No newline at end of file diff --git a/mowgli.py b/mowgli.py new file mode 100755 index 0000000..961871c --- /dev/null +++ b/mowgli.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +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', + 'Merp2': 'Error Reporting', + 'SLVT': 'Silverlight', +} + + +BASEURL = '/pr/%s/OfficeMac/' % UUID + + +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 check(pref='~/Library/Preferences/com.microsoft.autoupdate2.plist'): + """Collect info about available updates. + + """ + results = [] + + 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') + + 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' + conn = httplib.HTTPSConnection('officecdn.microsoft.com') + + conn.request("GET", BASEURL + filename % app_id) + data = conn.getresponse().read() + + 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']: + 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): + 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): + subprocess.call(['/usr/sbin/installer', '-pkg', pkg, '-target', '/']) + os.remove(pkg) + + +if __name__ == '__main__': + + if len(sys.argv) < 2: + print("usage: {0} [-l | -i [pref]".format(os.path.basename(sys.argv[0]))) + sys.exit(1) + + print("* Checking for updates...") + + try: + if os.path.exists(sys.argv[2]): + updates = check(sys.argv[2]) + except IndexError as e: + pass + + updates = [u for u in updates if u['needs_update']] + + if sys.argv[1] == '-l': + for u in updates: + print("{id}\t{name}\t{installed}\t{available}\t{size}".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) |