| OLD | NEW | 
|---|
| (Empty) |  | 
|  | 1 # This Source Code Form is subject to the terms of the Mozilla Public | 
|  | 2 # License, v. 2.0. If a copy of the MPL was not distributed with this | 
|  | 3 # file, You can obtain one at http://mozilla.org/MPL/2.0/. | 
|  | 4 """ | 
|  | 5 Track differences between versions of arbitrary extensions. | 
|  | 6 | 
|  | 7 Running this module direcly will attempt to fetch new versions for every | 
|  | 8 in ~/extwatcher.ini or /etc/extwatcher.ini configured and enabled extension | 
|  | 9 from the Chrome Web Store. When no parameters are given, you will simply be | 
|  | 10 notified about whether a new version was found. | 
|  | 11 | 
|  | 12 Providing -k/--keep-repository will leave the git repository (with unstaged | 
|  | 13 differences between the latest tracked version and the newest) on your drive, | 
|  | 14 in order to examine the differences locally. | 
|  | 15 | 
|  | 16 Providing -p/--push will stage all differences and push them to the configured | 
|  | 17 tracking repository. | 
|  | 18 """ | 
|  | 19 from __future__ import print_function | 
|  | 20 | 
|  | 21 import argparse | 
|  | 22 from configparser import ConfigParser, NoSectionError, NoOptionError | 
|  | 23 import json | 
|  | 24 import logging | 
|  | 25 import os | 
|  | 26 import shutil | 
|  | 27 import subprocess | 
|  | 28 import sys | 
|  | 29 import tempfile | 
|  | 30 from zipfile import ZipFile | 
|  | 31 | 
|  | 32 try:  # Python 3 only | 
|  | 33     from urllib import request as urllib | 
|  | 34 except ImportError:  # Python 2 only | 
|  | 35     import urllib | 
|  | 36 from xml.etree import ElementTree | 
|  | 37 | 
|  | 38 logging.basicConfig(level=logging.INFO) | 
|  | 39 | 
|  | 40 OMAHA_URL = 'https://omahaproxy.appspot.com/all.json?os=win' | 
|  | 41 SERVICE_URL = 'https://clients2.google.com/service/update2/crx' | 
|  | 42 | 
|  | 43 | 
|  | 44 class SensitiveFilter(logging.Filter): | 
|  | 45     """Filter sensitive ouput from the logs. | 
|  | 46 | 
|  | 47     This class is meant to be registered in python's logging facility. It will | 
|  | 48     mask potentially sensitive data from the message that is to be logged. | 
|  | 49 | 
|  | 50 | 
|  | 51     """ | 
|  | 52 | 
|  | 53     def __init__(self, patterns): | 
|  | 54         """Initialize a SensitiveFilter with the desired patterns.""" | 
|  | 55         self.patterns = patterns | 
|  | 56         super(SensitiveFilter, self).__init__() | 
|  | 57 | 
|  | 58     def filter(self, record): | 
|  | 59         msg = record.msg | 
|  | 60         if isinstance(msg, BaseException): | 
|  | 61             msg = str(msg) | 
|  | 62         record.msg = self.mask(msg) | 
|  | 63         return True | 
|  | 64 | 
|  | 65     def mask(self, msg): | 
|  | 66         try: | 
|  | 67             for pattern in self.patterns: | 
|  | 68                 msg = msg.replace( | 
|  | 69                     pattern, | 
|  | 70                     '/'.join(['******', pattern.rsplit('/', 1)[-1]]), | 
|  | 71                 ) | 
|  | 72         except AttributeError: | 
|  | 73             pass | 
|  | 74         return msg | 
|  | 75 | 
|  | 76 | 
|  | 77 class ConfigurationError(Exception): | 
|  | 78     pass | 
|  | 79 | 
|  | 80 | 
|  | 81 def read_config(path=None): | 
|  | 82     config_path = path or [ | 
|  | 83         os.path.expanduser('~/watchextensions.ini'), | 
|  | 84         '/etc/watchextensions.ini', | 
|  | 85     ] | 
|  | 86 | 
|  | 87     config = ConfigParser() | 
|  | 88     if not config.read(config_path): | 
|  | 89         raise ConfigurationError('No configuration file was found. Please ' | 
|  | 90                                  'provide ~/watchextensions.ini or ' | 
|  | 91                                  '/etc/watchextensions.ini or specify a valid ' | 
|  | 92                                  'path.') | 
|  | 93     return config | 
|  | 94 | 
|  | 95 | 
|  | 96 class ExtWatcher(object): | 
|  | 97     section = 'extensions' | 
|  | 98 | 
|  | 99     def __init__(self, ext_name, config, push, keep_repo): | 
|  | 100         self.logger = logging.getLogger(name=ext_name) | 
|  | 101         self.config = config | 
|  | 102 | 
|  | 103         self.ext_name = ext_name | 
|  | 104         self.push = push | 
|  | 105         self.keep_repo = keep_repo | 
|  | 106 | 
|  | 107         self._cws_ext_url = None | 
|  | 108         self._current_ext_version = None | 
|  | 109 | 
|  | 110         self.downloaded_file = None | 
|  | 111 | 
|  | 112         super(ExtWatcher, self).__init__() | 
|  | 113 | 
|  | 114     def _git_cmd(self, cmds, relative=False): | 
|  | 115         base = ['git'] | 
|  | 116         if not relative: | 
|  | 117             base += ['-C', self.tempdir] | 
|  | 118         suffix = [] | 
|  | 119         if not any(x in cmds for x in ['status', 'add', 'diff']): | 
|  | 120             suffix += ['--quiet'] | 
|  | 121         return subprocess.check_output(base + list(cmds) + suffix) | 
|  | 122 | 
|  | 123     def _clone_repository(self): | 
|  | 124         self.logger.info('Cloning ' + self.repository) | 
|  | 125         self._git_cmd(['clone', '-b', 'master', '--single-branch', | 
|  | 126                        self.repository, self.tempdir], | 
|  | 127                       relative=True) | 
|  | 128 | 
|  | 129     def _parse_config(self): | 
|  | 130         err_msg = '"{}" is not fully configured!'.format(self.ext_name) | 
|  | 131         try: | 
|  | 132             self.ext_id = self.config.get(self.section, self.ext_name + '_id') | 
|  | 133             self.repository = self.config.get(self.section, | 
|  | 134                                               self.ext_name + '_repository') | 
|  | 135             if not self.ext_id or not self.repository: | 
|  | 136                 raise ConfigurationError(err_msg) | 
|  | 137         except NoOptionError: | 
|  | 138             raise ConfigurationError(err_msg) | 
|  | 139         except NoSectionError: | 
|  | 140             raise ConfigurationError('Section [{}] is missing!' | 
|  | 141                                      ''.format(self.section)) | 
|  | 142 | 
|  | 143         self.logger.addFilter(SensitiveFilter([self.repository])) | 
|  | 144 | 
|  | 145     def __enter__(self): | 
|  | 146         self.tempdir = tempfile.mkdtemp(prefix='watched_extension_') | 
|  | 147         return self | 
|  | 148 | 
|  | 149     def __exit__(self, exc_type, exc_value, exc_traceback): | 
|  | 150         if exc_value: | 
|  | 151             self.logger.error(exc_value) | 
|  | 152 | 
|  | 153         if not self.keep_repo: | 
|  | 154             shutil.rmtree(self.tempdir, ignore_errors=True) | 
|  | 155         else: | 
|  | 156             print('Repository for {} available at {}'.format(self.ext_name, | 
|  | 157                                                              self.tempdir)) | 
|  | 158         if self.downloaded_file: | 
|  | 159             os.remove(self.downloaded_file) | 
|  | 160 | 
|  | 161         return True | 
|  | 162 | 
|  | 163     def _fetch_latest_chrome_version(self): | 
|  | 164         response = urllib.urlopen(OMAHA_URL).read() | 
|  | 165 | 
|  | 166         data = json.loads(response.decode('utf-8')) | 
|  | 167 | 
|  | 168         stable = [x for x in data[0]['versions'] if x['channel'] == 'stable'] | 
|  | 169         return stable[0]['current_version'] | 
|  | 170 | 
|  | 171     @property | 
|  | 172     def cws_ext_url(self): | 
|  | 173         if not self._cws_ext_url: | 
|  | 174             ext_url = SERVICE_URL + '?prodversion={}&x=id%3D{}%26uc'.format( | 
|  | 175                 self._fetch_latest_chrome_version(), self.ext_id, | 
|  | 176             ) | 
|  | 177             self._cws_ext_url = ext_url | 
|  | 178 | 
|  | 179         return self._cws_ext_url | 
|  | 180 | 
|  | 181     @property | 
|  | 182     def current_ext_version(self): | 
|  | 183         if not self._current_ext_version: | 
|  | 184             updatecheck_url = self.cws_ext_url + '&response=updatecheck' | 
|  | 185             manifest = urllib.urlopen(updatecheck_url).read() | 
|  | 186             root = ElementTree.fromstring(manifest) | 
|  | 187 | 
|  | 188             ns = {'g': 'http://www.google.com/update2/response'} | 
|  | 189             update = root.find('g:app/g:updatecheck', ns) | 
|  | 190 | 
|  | 191             self._current_ext_version = update.attrib['version'] | 
|  | 192         return self._current_ext_version | 
|  | 193 | 
|  | 194     def _download_ext_crx(self): | 
|  | 195         download_url = self.cws_ext_url + '&response=redirect' | 
|  | 196         filename = '{}-{}.crx'.format(self.ext_id, self.current_ext_version) | 
|  | 197 | 
|  | 198         urllib.urlretrieve(download_url, filename) | 
|  | 199         self.downloaded_file = filename | 
|  | 200         self.logger.info('Downloaded ' + filename) | 
|  | 201 | 
|  | 202     def _unzip_to_repository(self): | 
|  | 203         with ZipFile(self.downloaded_file, 'r') as zip_fp: | 
|  | 204             zip_fp.extractall(self.tempdir) | 
|  | 205 | 
|  | 206     def _get_tracked_version(self): | 
|  | 207         return [x.decode() for x in | 
|  | 208                 self._git_cmd(['log', '--pretty=format:%s']).splitlines()] | 
|  | 209 | 
|  | 210     def _track_new_contents(self, message): | 
|  | 211         status = self._git_cmd(['status']) | 
|  | 212         if b'nothing to commit' not in status: | 
|  | 213             self._git_cmd(['add', '--all']) | 
|  | 214             self._git_cmd(['commit', '-m', message]) | 
|  | 215             self._git_cmd(['push', 'origin', 'master']) | 
|  | 216 | 
|  | 217     def run(self): | 
|  | 218         self._parse_config() | 
|  | 219         self._clone_repository() | 
|  | 220 | 
|  | 221         next_version = self.current_ext_version | 
|  | 222         if next_version not in self._get_tracked_version(): | 
|  | 223             self.logger.info('New untracked version {} found!' | 
|  | 224                              ''.format(next_version)) | 
|  | 225             if self.push or self.keep_repo: | 
|  | 226                 self._download_ext_crx() | 
|  | 227                 self._unzip_to_repository() | 
|  | 228             if self.push: | 
|  | 229                 self._track_new_contents(next_version) | 
|  | 230         else: | 
|  | 231             self.logger.info('No new version found.') | 
|  | 232 | 
|  | 233 | 
|  | 234 if __name__ == '__main__': | 
|  | 235     parser = argparse.ArgumentParser( | 
|  | 236         description=__doc__.strip('\n'), | 
|  | 237         formatter_class=argparse.RawDescriptionHelpFormatter, | 
|  | 238     ) | 
|  | 239     parser.add_argument('-q', '--quiet', action='store_true', default=False, | 
|  | 240                         help='Suppress informational output.') | 
|  | 241     parser.add_argument('-p', '--push', action='store_true', default=False, | 
|  | 242                         help='Perfom a PUSH to the tracking repository.') | 
|  | 243     parser.add_argument('-k', '--keep-repository', action='store_true', | 
|  | 244                         default=False, help='Keep the local repository') | 
|  | 245     parser.add_argument('-c', '--config-path', default=None, | 
|  | 246                         help='Absolute path to a custom config file.') | 
|  | 247 | 
|  | 248     args = parser.parse_args() | 
|  | 249     if args.quiet: | 
|  | 250         logging.disable(logging.INFO) | 
|  | 251 | 
|  | 252     try: | 
|  | 253         config = read_config(args.config_path) | 
|  | 254 | 
|  | 255         for ext_name in config.options('enabled'): | 
|  | 256             with ExtWatcher(ext_name, config, args.push, | 
|  | 257                             args.keep_repository) as watcher: | 
|  | 258                 watcher.run() | 
|  | 259     except ConfigurationError as e: | 
|  | 260         sys.exit(e.message) | 
| OLD | NEW | 
|---|