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