samples_data_source.py revision 2a99a7e74a7f215066514fe81d2bfa6639d9eddd
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 re
9
10import compiled_file_system as compiled_fs
11from file_system import FileNotFoundError
12import third_party.json_schema_compiler.json_comment_eater as json_comment_eater
13import third_party.json_schema_compiler.model as model
14import url_constants
15
16DEFAULT_ICON_PATH = '/images/sample-default-icon.png'
17
18# See api_data_source.py for more info on _VERSION.
19_VERSION = 3
20
21class SamplesDataSource(object):
22  """Constructs a list of samples and their respective files and api calls.
23  """
24
25  class Factory(object):
26    """A factory to create SamplesDataSource instances bound to individual
27    Requests.
28    """
29    def __init__(self,
30                 channel,
31                 file_system,
32                 github_file_system,
33                 cache_factory,
34                 github_cache_factory,
35                 ref_resolver_factory,
36                 samples_path):
37      self._file_system = file_system
38      self._github_file_system = github_file_system
39      self._static_path = '/%s/static' % channel
40      self._extensions_cache = cache_factory.Create(self._MakeSamplesList,
41                                                    compiled_fs.EXTENSIONS,
42                                                    version=_VERSION)
43      self._apps_cache = github_cache_factory.Create(
44          lambda x, y: self._MakeSamplesList(x, y, is_apps=True),
45          compiled_fs.APPS,
46          version=_VERSION)
47      self._ref_resolver = ref_resolver_factory.Create()
48      self._samples_path = samples_path
49
50    def Create(self, request):
51      """Returns a new SamplesDataSource bound to |request|.
52      """
53      return SamplesDataSource(self._extensions_cache,
54                               self._apps_cache,
55                               self._samples_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')
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)
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      file_system = self._github_file_system if is_apps else self._file_system
103      samples_list = []
104      for filename in sorted(files):
105        if filename.rsplit('/')[-1] != 'manifest.json':
106          continue
107        # This is a little hacky, but it makes a sample page.
108        sample_path = filename.rsplit('/', 1)[-2]
109        sample_files = [path for path in files
110                        if path.startswith(sample_path + '/')]
111        js_files = [path for path in sample_files if path.endswith('.js')]
112        try:
113          js_contents = file_system.Read(js_files).Get()
114        except FileNotFoundError as e:
115          logging.warning('Error fetching samples files: %s. Was a file '
116                          'deleted from a sample? This warning should go away '
117                          'in 5 minutes.' % e)
118          continue
119        api_items = set()
120        for js in js_contents.values():
121          api_items.update(self._GetAPIItems(js))
122
123        api_calls = []
124        for item in sorted(api_items):
125          if len(item.split('.')) < 3:
126            continue
127          if item.endswith('.removeListener') or item.endswith('.hasListener'):
128            continue
129          if item.endswith('.addListener'):
130            item = item[:-len('.addListener')]
131          if item.startswith('chrome.'):
132            item = item[len('chrome.'):]
133          ref_data = self._ref_resolver.GetLink(item, 'samples')
134          if ref_data is None:
135            continue
136          api_calls.append({
137            'name': ref_data['text'],
138            'link': ref_data['href']
139          })
140        try:
141          manifest_data = self._GetDataFromManifest(sample_path, file_system)
142        except FileNotFoundError as e:
143          logging.warning('Error getting data from samples manifest: %s. If '
144                          'this file was deleted from a sample this message '
145                          'should go away in 5 minutes.' % e)
146          continue
147        if manifest_data is None:
148          continue
149
150        sample_base_path = sample_path.split('/', 1)[1]
151        if is_apps:
152          url = url_constants.GITHUB_BASE + '/' + sample_base_path
153          icon_base = url_constants.RAW_GITHUB_BASE + '/' + sample_base_path
154          download_url = url
155        else:
156          url = sample_base_path
157          icon_base = sample_base_path
158          download_url = sample_base_path + '.zip'
159
160        if manifest_data['icon'] is None:
161          icon_path = self._static_path + DEFAULT_ICON_PATH
162        else:
163          icon_path = icon_base + '/' + manifest_data['icon']
164        manifest_data.update({
165          'icon': icon_path,
166          'id': hashlib.md5(url).hexdigest(),
167          'download_url': download_url,
168          'url': url,
169          'files': [f.replace(sample_path + '/', '') for f in sample_files],
170          'api_calls': api_calls
171        })
172        samples_list.append(manifest_data)
173
174      return samples_list
175
176  def __init__(self, extensions_cache, apps_cache, samples_path, request):
177    self._extensions_cache = extensions_cache
178    self._apps_cache = apps_cache
179    self._samples_path = samples_path
180    self._request = request
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    api_search = api_name + '_'
195    samples_list = []
196    try:
197      for sample in self.get(key):
198        api_calls_unix = [model.UnixName(call['name'])
199                          for call in sample['api_calls']]
200        for call in api_calls_unix:
201          if call.startswith(api_search):
202            samples_list.append(sample)
203            break
204    except NotImplementedError:
205      # If we're testing, the GithubFileSystem can't fetch samples.
206      # Bug: http://crbug.com/141910
207      return []
208    return samples_list
209
210  def _CreateSamplesDict(self, key):
211    if key == 'apps':
212      samples_list = self._apps_cache.GetFromFileListing('/')
213    else:
214      samples_list = self._extensions_cache.GetFromFileListing(
215          self._samples_path + '/')
216    return_list = []
217    for dict_ in samples_list:
218      name = dict_['name']
219      description = dict_['description']
220      if description is None:
221        description = ''
222      if name.startswith('__MSG_') or description.startswith('__MSG_'):
223        try:
224          # Copy the sample dict so we don't change the dict in the cache.
225          sample_data = dict_.copy()
226          name_key = name[len('__MSG_'):-len('__')]
227          description_key = description[len('__MSG_'):-len('__')]
228          locale = sample_data['default_locale']
229          for lang in self._GetAcceptedLanguages():
230            if lang in sample_data['locales']:
231              locale = lang
232              break
233          locale_data = sample_data['locales'][locale]
234          sample_data['name'] = locale_data[name_key]['message']
235          sample_data['description'] = locale_data[description_key]['message']
236        except Exception as e:
237          logging.error(e)
238          # Revert the sample to the original dict.
239          sample_data = dict_
240        return_list.append(sample_data)
241      else:
242        return_list.append(dict_)
243    return return_list
244
245  def get(self, key):
246    return {
247      'apps': lambda: self._CreateSamplesDict('apps'),
248      'extensions': lambda: self._CreateSamplesDict('extensions')
249    }.get(key, lambda: {})()
250