1# Copyright 2013 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 posixpath
6
7from compiled_file_system import Cache, SingleFile, Unicode
8from extensions_paths import API_PATHS
9from features_bundle import HasParent, GetParentName
10from file_system import FileNotFoundError
11from future import All, Future, Race
12from operator import itemgetter
13from path_util import Join
14from platform_util import PlatformToExtensionType
15from schema_processor import SchemaProcessor, SchemaProcessorFactory
16from third_party.json_schema_compiler.json_schema import DeleteNodes
17from third_party.json_schema_compiler.model import Namespace, UnixName
18
19
20def GetNodeCategories():
21  '''Returns a tuple of the possible categories a node may belong to.
22  '''
23  return ('types', 'functions', 'events', 'properties')
24
25
26class ContentScriptAPI(object):
27  '''Represents an API available to content scripts.
28
29  |name| is the name of the API or API node this object represents.
30  |restrictedTo| is a list of dictionaries representing the nodes
31  of this API that are available to content scripts, or None if the
32  entire API is available to content scripts.
33  '''
34  def __init__(self, name):
35    self.name = name
36    self.restrictedTo = None
37
38  def __eq__(self, o):
39    return self.name == o.name and self.restrictedTo == o.restrictedTo
40
41  def __ne__(self, o):
42    return not (self == o)
43
44  def __repr__(self):
45    return '<ContentScriptAPI name=%s, restrictedTo=%s>' % (name, restrictedTo)
46
47  def __str__(self):
48    return repr(self)
49
50
51class APIModels(object):
52  '''Tracks APIs and their Models.
53  '''
54
55  def __init__(self,
56               features_bundle,
57               compiled_fs_factory,
58               file_system,
59               object_store_creator,
60               platform,
61               schema_processor_factory):
62    self._features_bundle = features_bundle
63    self._platform = PlatformToExtensionType(platform)
64    self._model_cache = compiled_fs_factory.Create(
65        file_system, self._CreateAPIModel, APIModels, category=self._platform)
66    self._object_store = object_store_creator.Create(APIModels)
67    self._schema_processor = Future(callback=lambda:
68                                    schema_processor_factory.Create(False))
69
70  @Cache
71  @SingleFile
72  @Unicode
73  def _CreateAPIModel(self, path, data):
74    def does_not_include_platform(node):
75      return ('extension_types' in node and
76              node['extension_types'] != 'all' and
77              self._platform not in node['extension_types'])
78
79    schema = self._schema_processor.Get().Process(path, data)[0]
80    if not schema:
81      raise ValueError('No schema for %s' % path)
82    return Namespace(DeleteNodes(
83        schema, matcher=does_not_include_platform), path)
84
85  def GetNames(self):
86    # API names appear alongside some of their methods/events/etc in the
87    # features file. APIs are those which either implicitly or explicitly have
88    # no parent feature (e.g. app, app.window, and devtools.inspectedWindow are
89    # APIs; runtime.onConnectNative is not).
90    api_features = self._features_bundle.GetAPIFeatures().Get()
91    return [name for name, feature in api_features.iteritems()
92            if not HasParent(name, feature, api_features)]
93
94  def _GetPotentialPathsForModel(self, api_name):
95    '''Returns the list of file system paths that the model for |api_name|
96    might be located at.
97    '''
98    # By default |api_name| is assumed to be given without a path or extension,
99    # so combinations of known paths and extension types will be searched.
100    api_extensions = ('.json', '.idl')
101    api_paths = API_PATHS
102
103    # Callers sometimes include a file extension and/or prefix path with the
104    # |api_name| argument. We believe them and narrow the search space
105    # accordingly.
106    name, ext = posixpath.splitext(api_name)
107    if ext in api_extensions:
108      api_extensions = (ext,)
109      api_name = name
110    for api_path in api_paths:
111      if api_name.startswith(api_path):
112        api_name = api_name[len(api_path):]
113        api_paths = (api_path,)
114        break
115
116    # API names are given as declarativeContent and app.window but file names
117    # will be declarative_content and app_window.
118    file_name = UnixName(api_name).replace('.', '_')
119    # Devtools APIs are in API/devtools/ not API/, and have their
120    # "devtools" names removed from the file names.
121    basename = posixpath.basename(file_name)
122    if 'devtools_' in basename:
123      file_name = posixpath.join(
124          'devtools', file_name.replace(basename,
125                                        basename.replace('devtools_' , '')))
126
127    return [Join(path, file_name + ext) for ext in api_extensions
128                                        for path in api_paths]
129
130  def GetModel(self, api_name):
131    futures = [self._model_cache.GetFromFile(path)
132               for path in self._GetPotentialPathsForModel(api_name)]
133    return Race(futures, except_pass=(FileNotFoundError, ValueError))
134
135  def GetContentScriptAPIs(self):
136    '''Creates a dict of APIs and nodes supported by content scripts in
137    this format:
138
139      {
140        'extension': '<ContentScriptAPI name='extension',
141                                        restrictedTo=[{'node': 'onRequest'}]>',
142        ...
143      }
144    '''
145    content_script_apis_future = self._object_store.Get('content_script_apis')
146    api_features_future = self._features_bundle.GetAPIFeatures()
147    def resolve():
148      content_script_apis = content_script_apis_future.Get()
149      if content_script_apis is not None:
150        return content_script_apis
151
152      api_features = api_features_future.Get()
153      content_script_apis = {}
154      for name, feature in api_features.iteritems():
155        if 'content_script' not in feature.get('contexts', ()):
156          continue
157        parent = GetParentName(name, feature, api_features)
158        if parent is None:
159          content_script_apis[name] = ContentScriptAPI(name)
160        else:
161          # Creates a dict for the individual node.
162          node = {'node': name[len(parent) + 1:]}
163          if parent not in content_script_apis:
164            content_script_apis[parent] = ContentScriptAPI(parent)
165          if content_script_apis[parent].restrictedTo:
166            content_script_apis[parent].restrictedTo.append(node)
167          else:
168            content_script_apis[parent].restrictedTo = [node]
169
170      self._object_store.Set('content_script_apis', content_script_apis)
171      return content_script_apis
172    return Future(callback=resolve)
173
174  def Refresh(self):
175    futures = [self.GetModel(name) for name in self.GetNames()]
176    return All(futures, except_pass=(FileNotFoundError, ValueError))
177
178  def IterModels(self):
179    future_models = [(name, self.GetModel(name)) for name in self.GetNames()]
180    for name, future_model in future_models:
181      try:
182        model = future_model.Get()
183      except FileNotFoundError:
184        continue
185      if model:
186        yield name, model
187