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