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