samples_data_source.py revision 1e9bf3e0803691d0a228da41fc608347b6db4340
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
13import third_party.json_schema_compiler.json_comment_eater as json_comment_eater
14import third_party.json_schema_compiler.model as model
15import url_constants
16
17DEFAULT_ICON_PATH = 'images/sample-default-icon.png'
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                 ref_resolver_factory,
31                 extension_samples_path,
32                 base_path):
33      self._host_file_system = host_file_system
34      self._app_samples_file_system = app_samples_file_system
35      self._ref_resolver = ref_resolver_factory.Create()
36      self._extension_samples_path = extension_samples_path
37      self._base_path = base_path
38      self._extensions_cache = compiled_fs_factory.Create(
39          host_file_system,
40          self._MakeSamplesList,
41          SamplesDataSource,
42          category='extensions')
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
49    def Create(self, request):
50      '''Returns a new SamplesDataSource bound to |request|.
51      '''
52      return SamplesDataSource(self._extensions_cache,
53                               self._apps_cache,
54                               self._extension_samples_path,
55                               self._base_path,
56                               request)
57
58    def _GetAPIItems(self, js_file):
59      chrome_regex = '(chrome\.[a-zA-Z0-9\.]+)'
60      calls = set(re.findall(chrome_regex, js_file))
61      # Find APIs that have been assigned into variables.
62      assigned_vars = dict(re.findall('var\s*([^\s]+)\s*=\s*%s;' % chrome_regex,
63                                      js_file))
64      # Replace the variable name with the full API name.
65      for var_name, value in assigned_vars.iteritems():
66        js_file = js_file.replace(var_name, value)
67      return calls.union(re.findall(chrome_regex, js_file))
68
69    def _GetDataFromManifest(self, path, file_system):
70      manifest = file_system.ReadSingle(path + '/manifest.json').Get()
71      try:
72        manifest_json = json.loads(json_comment_eater.Nom(manifest))
73      except ValueError as e:
74        logging.error('Error parsing manifest.json for %s: %s' % (path, e))
75        return None
76      l10n_data = {
77        'name': manifest_json.get('name', ''),
78        'description': manifest_json.get('description', None),
79        'icon': manifest_json.get('icons', {}).get('128', None),
80        'default_locale': manifest_json.get('default_locale', None),
81        'locales': {}
82      }
83      if not l10n_data['default_locale']:
84        return l10n_data
85      locales_path = path + '/_locales/'
86      locales_dir = file_system.ReadSingle(locales_path).Get()
87      if locales_dir:
88        locales_files = file_system.Read(
89            [locales_path + f + 'messages.json' for f in locales_dir]).Get()
90        try:
91          locales_json = [(locale_path, json.loads(contents))
92                          for locale_path, contents in
93                          locales_files.iteritems()]
94        except ValueError as e:
95          logging.error('Error parsing locales files for %s: %s' % (path, e))
96        else:
97          for path, json_ in locales_json:
98            l10n_data['locales'][path[len(locales_path):].split('/')[0]] = json_
99      return l10n_data
100
101    def _MakeSamplesList(self, base_dir, files, is_apps=False):
102      # HACK(kalman): The code here (for legacy reasons) assumes that |files| is
103      # prefixed by |base_dir|, so make it true.
104      files = ['%s%s' % (base_dir, f) for f in files]
105      file_system = (self._app_samples_file_system if is_apps else
106                     self._host_file_system)
107      samples_list = []
108      for filename in sorted(files):
109        if filename.rsplit('/')[-1] != 'manifest.json':
110          continue
111
112        # This is a little hacky, but it makes a sample page.
113        sample_path = filename.rsplit('/', 1)[-2]
114        sample_files = [path for path in files
115                        if path.startswith(sample_path + '/')]
116        js_files = [path for path in sample_files if path.endswith('.js')]
117        js_contents = file_system.Read(js_files).Get()
118        api_items = set()
119        for js in js_contents.values():
120          api_items.update(self._GetAPIItems(js))
121
122        api_calls = []
123        for item in sorted(api_items):
124          if len(item.split('.')) < 3:
125            continue
126          if item.endswith('.removeListener') or item.endswith('.hasListener'):
127            continue
128          if item.endswith('.addListener'):
129            item = item[:-len('.addListener')]
130          if item.startswith('chrome.'):
131            item = item[len('chrome.'):]
132          ref_data = self._ref_resolver.GetLink(item)
133          if ref_data is None:
134            continue
135          api_calls.append({
136            'name': ref_data['text'],
137            'link': ref_data['href']
138          })
139
140        sample_base_path = sample_path.split('/', 1)[1]
141        if is_apps:
142          url = url_constants.GITHUB_BASE + '/' + sample_base_path
143          icon_base = url_constants.RAW_GITHUB_BASE + '/' + sample_base_path
144          download_url = url
145        else:
146          url = sample_base_path
147          icon_base = sample_base_path
148          download_url = sample_base_path + '.zip'
149
150        manifest_data = self._GetDataFromManifest(sample_path, file_system)
151        if manifest_data['icon'] is None:
152          icon_path = posixpath.join(
153              self._base_path, 'static', DEFAULT_ICON_PATH)
154        else:
155          icon_path = '%s/%s' % (icon_base, manifest_data['icon'])
156        manifest_data.update({
157          'icon': icon_path,
158          'download_url': download_url,
159          'url': url,
160          'files': [f.replace(sample_path + '/', '') for f in sample_files],
161          'api_calls': api_calls
162        })
163        samples_list.append(manifest_data)
164
165      return samples_list
166
167  def __init__(self,
168               extensions_cache,
169               apps_cache,
170               extension_samples_path,
171               base_path,
172               request):
173    self._extensions_cache = extensions_cache
174    self._apps_cache = apps_cache
175    self._extension_samples_path = extension_samples_path
176    self._base_path = base_path
177    self._request = request
178
179  def _GetSampleId(self, sample_name):
180    return sample_name.lower().replace(' ', '-')
181
182  def _GetAcceptedLanguages(self):
183    accept_language = self._request.headers.get('Accept-Language', None)
184    if accept_language is None:
185      return []
186    return [lang_with_q.split(';')[0].strip()
187            for lang_with_q in accept_language.split(',')]
188
189  def FilterSamples(self, key, api_name):
190    '''Fetches and filters the list of samples specified by |key|, returning
191    only the samples that use the API |api_name|. |key| is either 'apps' or
192    'extensions'.
193    '''
194    return [sample for sample in self.get(key) if any(
195        call['name'].startswith(api_name + '.')
196        for call in sample['api_calls'])]
197
198  def _CreateSamplesDict(self, key):
199    if key == 'apps':
200      samples_list = self._apps_cache.GetFromFileListing('/').Get()
201    else:
202      samples_list = self._extensions_cache.GetFromFileListing(
203          self._extension_samples_path + '/').Get()
204    return_list = []
205    for dict_ in samples_list:
206      name = dict_['name']
207      description = dict_['description']
208      if description is None:
209        description = ''
210      if name.startswith('__MSG_') or description.startswith('__MSG_'):
211        try:
212          # Copy the sample dict so we don't change the dict in the cache.
213          sample_data = dict_.copy()
214          name_key = name[len('__MSG_'):-len('__')]
215          description_key = description[len('__MSG_'):-len('__')]
216          locale = sample_data['default_locale']
217          for lang in self._GetAcceptedLanguages():
218            if lang in sample_data['locales']:
219              locale = lang
220              break
221          locale_data = sample_data['locales'][locale]
222          sample_data['name'] = locale_data[name_key]['message']
223          sample_data['description'] = locale_data[description_key]['message']
224          sample_data['id'] = self._GetSampleId(sample_data['name'])
225        except Exception as e:
226          logging.error(traceback.format_exc())
227          # Revert the sample to the original dict.
228          sample_data = dict_
229        return_list.append(sample_data)
230      else:
231        dict_['id'] = self._GetSampleId(name)
232        return_list.append(dict_)
233    return return_list
234
235  def get(self, key):
236    return {
237      'apps': lambda: self._CreateSamplesDict('apps'),
238      'extensions': lambda: self._CreateSamplesDict('extensions')
239    }.get(key, lambda: {})()
240