samples_data_source.py revision a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7
1# Copyright (c) 2012 The Chromium Authors. All rights reserved. 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5import hashlib 6import json 7import logging 8import posixpath 9import re 10import traceback 11 12from compiled_file_system import CompiledFileSystem 13from extensions_paths import EXAMPLES 14import third_party.json_schema_compiler.json_comment_eater as json_comment_eater 15import third_party.json_schema_compiler.model as model 16import url_constants 17 18 19_DEFAULT_ICON_PATH = 'images/sample-default-icon.png' 20 21 22class SamplesDataSource(object): 23 '''Constructs a list of samples and their respective files and api calls. 24 ''' 25 class Factory(object): 26 '''A factory to create SamplesDataSource instances bound to individual 27 Requests. 28 ''' 29 def __init__(self, 30 host_file_system, 31 app_samples_file_system, 32 compiled_fs_factory, 33 ref_resolver_factory, 34 base_path): 35 self._host_file_system = host_file_system 36 self._app_samples_file_system = app_samples_file_system 37 self._ref_resolver = ref_resolver_factory.Create() 38 self._base_path = base_path 39 self._extensions_cache = compiled_fs_factory.Create( 40 host_file_system, 41 self._MakeSamplesList, 42 SamplesDataSource, 43 category='extensions') 44 self._extensions_text_cache = compiled_fs_factory.ForUnicode( 45 host_file_system) 46 self._apps_cache = compiled_fs_factory.Create( 47 app_samples_file_system, 48 lambda *args: self._MakeSamplesList(*args, is_apps=True), 49 SamplesDataSource, 50 category='apps') 51 self._apps_text_cache = compiled_fs_factory.ForUnicode( 52 app_samples_file_system) 53 54 def Create(self, request): 55 '''Returns a new SamplesDataSource bound to |request|. 56 ''' 57 return SamplesDataSource(self._extensions_cache, 58 self._apps_cache, 59 self._base_path, 60 request) 61 62 def _GetAPIItems(self, js_file): 63 chrome_pattern = r'chrome[\w.]+' 64 # Add API calls that appear normally, like "chrome.runtime.connect". 65 calls = set(re.findall(chrome_pattern, js_file)) 66 # Add API calls that have been assigned into variables, like 67 # "var storageArea = chrome.storage.sync; storageArea.get", which should 68 # be expanded like "chrome.storage.sync.get". 69 for match in re.finditer(r'var\s+(\w+)\s*=\s*(%s);' % chrome_pattern, 70 js_file): 71 var_name, api_prefix = match.groups() 72 for var_match in re.finditer(r'\b%s\.([\w.]+)\b' % re.escape(var_name), 73 js_file): 74 api_suffix, = var_match.groups() 75 calls.add('%s.%s' % (api_prefix, api_suffix)) 76 return calls 77 78 def _GetDataFromManifest(self, path, text_cache, file_system): 79 manifest = text_cache.GetFromFile(path + '/manifest.json').Get() 80 try: 81 manifest_json = json.loads(json_comment_eater.Nom(manifest)) 82 except ValueError as e: 83 logging.error('Error parsing manifest.json for %s: %s' % (path, e)) 84 return None 85 l10n_data = { 86 'name': manifest_json.get('name', ''), 87 'description': manifest_json.get('description', None), 88 'icon': manifest_json.get('icons', {}).get('128', None), 89 'default_locale': manifest_json.get('default_locale', None), 90 'locales': {} 91 } 92 if not l10n_data['default_locale']: 93 return l10n_data 94 locales_path = path + '/_locales/' 95 locales_dir = file_system.ReadSingle(locales_path).Get() 96 if locales_dir: 97 def load_locale_json(path): 98 return (path, json.loads(text_cache.GetFromFile(path).Get())) 99 100 try: 101 locales_json = [load_locale_json(locales_path + f + 'messages.json') 102 for f in locales_dir] 103 except ValueError as e: 104 logging.error('Error parsing locales files for %s: %s' % (path, e)) 105 else: 106 for path, json_ in locales_json: 107 l10n_data['locales'][path[len(locales_path):].split('/')[0]] = json_ 108 return l10n_data 109 110 def _MakeSamplesList(self, base_path, files, is_apps=False): 111 file_system = (self._app_samples_file_system if is_apps else 112 self._host_file_system) 113 text_cache = (self._apps_text_cache if is_apps else 114 self._extensions_text_cache) 115 samples_list = [] 116 for filename in sorted(files): 117 if filename.rsplit('/')[-1] != 'manifest.json': 118 continue 119 120 # This is a little hacky, but it makes a sample page. 121 sample_path = filename.rsplit('/', 1)[-2] 122 sample_files = [path for path in files 123 if path.startswith(sample_path + '/')] 124 js_files = [path for path in sample_files if path.endswith('.js')] 125 js_contents = [text_cache.GetFromFile( 126 posixpath.join(base_path, js_file)).Get() 127 for js_file in js_files] 128 api_items = set() 129 for js in js_contents: 130 api_items.update(self._GetAPIItems(js)) 131 132 api_calls = [] 133 for item in sorted(api_items): 134 if len(item.split('.')) < 3: 135 continue 136 if item.endswith('.removeListener') or item.endswith('.hasListener'): 137 continue 138 if item.endswith('.addListener'): 139 item = item[:-len('.addListener')] 140 if item.startswith('chrome.'): 141 item = item[len('chrome.'):] 142 ref_data = self._ref_resolver.GetLink(item) 143 # TODO(kalman): What about references like chrome.storage.sync.get? 144 # That should link to either chrome.storage.sync or 145 # chrome.storage.StorageArea.get (or probably both). 146 # TODO(kalman): Filter out API-only references? This can happen when 147 # the API namespace is assigned to a variable, but it's very hard to 148 # to disambiguate. 149 if ref_data is None: 150 continue 151 api_calls.append({ 152 'name': ref_data['text'], 153 'link': ref_data['href'] 154 }) 155 156 if is_apps: 157 url = url_constants.GITHUB_BASE + '/' + sample_path 158 icon_base = url_constants.RAW_GITHUB_BASE + '/' + sample_path 159 download_url = url 160 else: 161 extension_sample_path = posixpath.join('examples', sample_path) 162 url = extension_sample_path 163 icon_base = extension_sample_path 164 download_url = extension_sample_path + '.zip' 165 166 manifest_data = self._GetDataFromManifest( 167 posixpath.join(base_path, sample_path), 168 text_cache, 169 file_system) 170 if manifest_data['icon'] is None: 171 icon_path = posixpath.join( 172 self._base_path, 'static', _DEFAULT_ICON_PATH) 173 else: 174 icon_path = '%s/%s' % (icon_base, manifest_data['icon']) 175 manifest_data.update({ 176 'icon': icon_path, 177 'download_url': download_url, 178 'url': url, 179 'files': [f.replace(sample_path + '/', '') for f in sample_files], 180 'api_calls': api_calls 181 }) 182 samples_list.append(manifest_data) 183 184 return samples_list 185 186 def __init__(self, 187 extensions_cache, 188 apps_cache, 189 base_path, 190 request): 191 self._extensions_cache = extensions_cache 192 self._apps_cache = apps_cache 193 self._base_path = base_path 194 self._request = request 195 196 def _GetSampleId(self, sample_name): 197 return sample_name.lower().replace(' ', '-') 198 199 def _GetAcceptedLanguages(self): 200 accept_language = self._request.headers.get('Accept-Language', None) 201 if accept_language is None: 202 return [] 203 return [lang_with_q.split(';')[0].strip() 204 for lang_with_q in accept_language.split(',')] 205 206 def FilterSamples(self, key, api_name): 207 '''Fetches and filters the list of samples specified by |key|, returning 208 only the samples that use the API |api_name|. |key| is either 'apps' or 209 'extensions'. 210 ''' 211 return [sample for sample in self.get(key) if any( 212 call['name'].startswith(api_name + '.') 213 for call in sample['api_calls'])] 214 215 def _CreateSamplesDict(self, key): 216 if key == 'apps': 217 samples_list = self._apps_cache.GetFromFileListing('/').Get() 218 else: 219 samples_list = self._extensions_cache.GetFromFileListing(EXAMPLES).Get() 220 return_list = [] 221 for dict_ in samples_list: 222 name = dict_['name'] 223 description = dict_['description'] 224 if description is None: 225 description = '' 226 if name.startswith('__MSG_') or description.startswith('__MSG_'): 227 try: 228 # Copy the sample dict so we don't change the dict in the cache. 229 sample_data = dict_.copy() 230 name_key = name[len('__MSG_'):-len('__')] 231 description_key = description[len('__MSG_'):-len('__')] 232 locale = sample_data['default_locale'] 233 for lang in self._GetAcceptedLanguages(): 234 if lang in sample_data['locales']: 235 locale = lang 236 break 237 locale_data = sample_data['locales'][locale] 238 sample_data['name'] = locale_data[name_key]['message'] 239 sample_data['description'] = locale_data[description_key]['message'] 240 sample_data['id'] = self._GetSampleId(sample_data['name']) 241 except Exception as e: 242 logging.error(traceback.format_exc()) 243 # Revert the sample to the original dict. 244 sample_data = dict_ 245 return_list.append(sample_data) 246 else: 247 dict_['id'] = self._GetSampleId(name) 248 return_list.append(dict_) 249 return return_list 250 251 def get(self, key): 252 return { 253 'apps': lambda: self._CreateSamplesDict('apps'), 254 'extensions': lambda: self._CreateSamplesDict('extensions') 255 }.get(key, lambda: {})() 256