1 // Copyright 2017 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 
5 #include "extensions/renderer/bindings/api_last_error.h"
6 
7 #include "gin/converter.h"
8 #include "gin/data_object_builder.h"
9 #include "gin/handle.h"
10 #include "gin/object_template_builder.h"
11 #include "gin/wrappable.h"
12 
13 namespace extensions {
14 
15 namespace {
16 
17 constexpr char kLastErrorProperty[] = "lastError";
18 constexpr char kScriptSuppliedValueKey[] = "script_supplied_value";
19 constexpr char kUncheckedErrorPrefix[] = "Unchecked runtime.lastError: ";
20 
21 // The object corresponding to the lastError property, containing a single
22 // property ('message') with the last error. This object is stored on the parent
23 // (chrome.runtime in production) as a private property, and is returned via an
24 // accessor which marks the error as accessed.
25 class LastErrorObject final : public gin::Wrappable<LastErrorObject> {
26  public:
LastErrorObject(const std::string & error)27   explicit LastErrorObject(const std::string& error) : error_(error) {}
28 
29   static gin::WrapperInfo kWrapperInfo;
30 
31   // gin::Wrappable:
GetObjectTemplateBuilder(v8::Isolate * isolate)32   gin::ObjectTemplateBuilder GetObjectTemplateBuilder(
33       v8::Isolate* isolate) override {
34     DCHECK(isolate);
35     return Wrappable<LastErrorObject>::GetObjectTemplateBuilder(isolate)
36         .SetProperty("message", &LastErrorObject::error);
37   }
38 
Reset(const std::string & error)39   void Reset(const std::string& error) {
40     error_ = error;
41     accessed_ = false;
42   }
43 
error() const44   const std::string& error() const { return error_; }
accessed() const45   bool accessed() const { return accessed_; }
set_accessed()46   void set_accessed() { accessed_ = true; }
47 
48  private:
49   std::string error_;
50   bool accessed_ = false;
51 
52   DISALLOW_COPY_AND_ASSIGN(LastErrorObject);
53 };
54 
55 gin::WrapperInfo LastErrorObject::kWrapperInfo = {gin::kEmbedderNativeGin};
56 
57 // An accessor to retrieve the last error property (curried in through data),
58 // and mark it as accessed.
LastErrorGetter(v8::Local<v8::Name> property,const v8::PropertyCallbackInfo<v8::Value> & info)59 void LastErrorGetter(v8::Local<v8::Name> property,
60                      const v8::PropertyCallbackInfo<v8::Value>& info) {
61   v8::Isolate* isolate = info.GetIsolate();
62   v8::HandleScope handle_scope(isolate);
63   v8::Local<v8::Object> holder = info.Holder();
64   v8::Local<v8::Context> context = holder->CreationContext();
65 
66   v8::Local<v8::Value> last_error;
67   v8::Local<v8::Private> last_error_key = v8::Private::ForApi(
68       isolate, gin::StringToSymbol(isolate, kLastErrorProperty));
69   if (!holder->GetPrivate(context, last_error_key).ToLocal(&last_error) ||
70       last_error != info.Data()) {
71     // Something funny happened - our private properties aren't set right.
72     NOTREACHED();
73     return;
74   }
75 
76   v8::Local<v8::Value> return_value;
77 
78   // It's possible that some script has set their own value for the last error
79   // property. If so, return that. Otherwise, return the real last error.
80   v8::Local<v8::Private> script_value_key = v8::Private::ForApi(
81       isolate, gin::StringToSymbol(isolate, kScriptSuppliedValueKey));
82   v8::Local<v8::Value> script_value;
83   if (holder->GetPrivate(context, script_value_key).ToLocal(&script_value) &&
84       !script_value->IsUndefined()) {
85     return_value = script_value;
86   } else {
87     LastErrorObject* last_error_obj = nullptr;
88     CHECK(gin::Converter<LastErrorObject*>::FromV8(isolate, last_error,
89                                                    &last_error_obj));
90     last_error_obj->set_accessed();
91     return_value = last_error;
92   }
93 
94   info.GetReturnValue().Set(return_value);
95 }
96 
97 // Allow script to set the last error property.
LastErrorSetter(v8::Local<v8::Name> property,v8::Local<v8::Value> value,const v8::PropertyCallbackInfo<void> & info)98 void LastErrorSetter(v8::Local<v8::Name> property,
99                      v8::Local<v8::Value> value,
100                      const v8::PropertyCallbackInfo<void>& info) {
101   v8::Isolate* isolate = info.GetIsolate();
102   v8::HandleScope handle_scope(isolate);
103   v8::Local<v8::Object> holder = info.Holder();
104   v8::Local<v8::Context> context = holder->CreationContext();
105 
106   v8::Local<v8::Private> script_value_key = v8::Private::ForApi(
107       isolate, gin::StringToSymbol(isolate, kScriptSuppliedValueKey));
108   v8::Maybe<bool> set_private =
109       holder->SetPrivate(context, script_value_key, value);
110   if (!set_private.IsJust() || !set_private.FromJust())
111     NOTREACHED();
112 }
113 
114 }  // namespace
115 
APILastError(GetParent get_parent,binding::AddConsoleError add_console_error)116 APILastError::APILastError(GetParent get_parent,
117                            binding::AddConsoleError add_console_error)
118     : get_parent_(std::move(get_parent)),
119       add_console_error_(std::move(add_console_error)) {}
120 APILastError::APILastError(APILastError&& other) = default;
121 APILastError::~APILastError() = default;
122 
SetError(v8::Local<v8::Context> context,const std::string & error)123 void APILastError::SetError(v8::Local<v8::Context> context,
124                             const std::string& error) {
125   v8::Isolate* isolate = context->GetIsolate();
126   DCHECK(isolate);
127   v8::HandleScope handle_scope(isolate);
128 
129   // The various accesses/sets on an object could potentially fail if script has
130   // set any crazy interceptors. For the most part, we don't care about behaving
131   // perfectly in these circumstances, but we eat the exception so callers don't
132   // have to worry about it. We also SetVerbose() so that developers will have a
133   // clue what happened if this does arise.
134   // TODO(devlin): Whether or not this needs to be verbose is debatable.
135   v8::TryCatch try_catch(isolate);
136   try_catch.SetVerbose(true);
137 
138   v8::Local<v8::Object> secondary_parent;
139   v8::Local<v8::Object> parent = get_parent_.Run(context, &secondary_parent);
140 
141   SetErrorOnPrimaryParent(context, parent, error);
142   SetErrorOnSecondaryParent(context, secondary_parent, error);
143 }
144 
ClearError(v8::Local<v8::Context> context,bool report_if_unchecked)145 void APILastError::ClearError(v8::Local<v8::Context> context,
146                               bool report_if_unchecked) {
147   v8::Isolate* isolate = context->GetIsolate();
148   v8::HandleScope handle_scope(isolate);
149 
150   v8::Local<v8::Object> parent;
151   v8::Local<v8::Object> secondary_parent;
152   LastErrorObject* last_error = nullptr;
153   v8::Local<v8::String> key;
154   v8::Local<v8::Private> private_key;
155   {
156     // See comment in SetError().
157     v8::TryCatch try_catch(isolate);
158     try_catch.SetVerbose(true);
159 
160     parent = get_parent_.Run(context, &secondary_parent);
161     if (parent.IsEmpty())
162       return;
163     key = gin::StringToSymbol(isolate, kLastErrorProperty);
164     private_key = v8::Private::ForApi(isolate, key);
165     v8::Local<v8::Value> error;
166     // Access through GetPrivate() so that we don't trigger accessed().
167     if (!parent->GetPrivate(context, private_key).ToLocal(&error) ||
168         !gin::Converter<LastErrorObject*>::FromV8(context->GetIsolate(), error,
169                                                   &last_error)) {
170       return;
171     }
172   }
173 
174   if (report_if_unchecked && !last_error->accessed())
175     ReportUncheckedError(context, last_error->error());
176 
177   // See comment in SetError().
178   v8::TryCatch try_catch(isolate);
179   try_catch.SetVerbose(true);
180 
181   v8::Maybe<bool> delete_private = parent->DeletePrivate(context, private_key);
182   if (!delete_private.IsJust() || !delete_private.FromJust()) {
183     NOTREACHED();
184     return;
185   }
186   // These Delete()s can fail, but there's nothing to do if it does (the
187   // exception will be caught by the TryCatch above).
188   ignore_result(parent->Delete(context, key));
189   if (!secondary_parent.IsEmpty())
190     ignore_result(secondary_parent->Delete(context, key));
191 }
192 
HasError(v8::Local<v8::Context> context)193 bool APILastError::HasError(v8::Local<v8::Context> context) {
194   v8::Isolate* isolate = context->GetIsolate();
195   v8::HandleScope handle_scope(isolate);
196 
197   // See comment in SetError().
198   v8::TryCatch try_catch(isolate);
199   try_catch.SetVerbose(true);
200 
201   v8::Local<v8::Object> parent = get_parent_.Run(context, nullptr);
202   if (parent.IsEmpty())
203     return false;
204   v8::Local<v8::Value> error;
205   v8::Local<v8::Private> key = v8::Private::ForApi(
206       isolate, gin::StringToSymbol(isolate, kLastErrorProperty));
207   // Access through GetPrivate() so we don't trigger accessed().
208   if (!parent->GetPrivate(context, key).ToLocal(&error))
209     return false;
210 
211   LastErrorObject* last_error = nullptr;
212   return gin::Converter<LastErrorObject*>::FromV8(context->GetIsolate(), error,
213                                                   &last_error);
214 }
215 
ReportUncheckedError(v8::Local<v8::Context> context,const std::string & error)216 void APILastError::ReportUncheckedError(v8::Local<v8::Context> context,
217                                         const std::string& error) {
218   add_console_error_.Run(context, kUncheckedErrorPrefix + error);
219 }
220 
SetErrorOnPrimaryParent(v8::Local<v8::Context> context,v8::Local<v8::Object> parent,const std::string & error)221 void APILastError::SetErrorOnPrimaryParent(v8::Local<v8::Context> context,
222                                            v8::Local<v8::Object> parent,
223                                            const std::string& error) {
224   if (parent.IsEmpty())
225     return;
226   v8::Isolate* isolate = context->GetIsolate();
227   v8::Local<v8::String> key = gin::StringToSymbol(isolate, kLastErrorProperty);
228   v8::Local<v8::Value> v8_error;
229   // Two notes: this Get() is visible to external script, and this will actually
230   // mark the lastError as accessed, if one exists. These shouldn't be a
231   // problem (lastError is meant to be helpful, but isn't designed to handle
232   // crazy chaining, etc). However, if we decide we needed to be fancier, we
233   // could detect the presence of a current error through a GetPrivate(), and
234   // optionally throw it if one exists.
235   if (!parent->Get(context, key).ToLocal(&v8_error))
236     return;
237 
238   if (!v8_error->IsUndefined()) {
239     // There may be an existing last error to overwrite.
240     LastErrorObject* last_error = nullptr;
241     if (!gin::Converter<LastErrorObject*>::FromV8(isolate, v8_error,
242                                                   &last_error)) {
243       // If it's not a real lastError (e.g. if a script manually set it), don't
244       // do anything. We shouldn't mangle a property set by other script.
245       // TODO(devlin): Or should we? If someone sets chrome.runtime.lastError,
246       // it might be the right course of action to overwrite it.
247       return;
248     }
249     last_error->Reset(error);
250   } else {
251     v8::Local<v8::Value> last_error =
252         gin::CreateHandle(isolate, new LastErrorObject(error)).ToV8();
253     v8::Maybe<bool> set_private = parent->SetPrivate(
254         context, v8::Private::ForApi(isolate, key), last_error);
255     if (!set_private.IsJust() || !set_private.FromJust()) {
256       NOTREACHED();
257       return;
258     }
259     DCHECK(!last_error.IsEmpty());
260     // This SetAccessor() can fail, but there's nothing to do if it does (the
261     // exception will be caught by the TryCatch in SetError()).
262     ignore_result(parent->SetAccessor(context, key, &LastErrorGetter,
263                                       &LastErrorSetter, last_error));
264   }
265 }
266 
SetErrorOnSecondaryParent(v8::Local<v8::Context> context,v8::Local<v8::Object> secondary_parent,const std::string & error)267 void APILastError::SetErrorOnSecondaryParent(
268     v8::Local<v8::Context> context,
269     v8::Local<v8::Object> secondary_parent,
270     const std::string& error) {
271   if (secondary_parent.IsEmpty())
272     return;
273 
274   // For the secondary parent, simply set chrome.extension.lastError to
275   // {message: <error>}.
276   // TODO(devlin): Gather metrics on how frequently this is checked. It'd be
277   // nice to get rid of it.
278   v8::Isolate* isolate = context->GetIsolate();
279   v8::Local<v8::String> key = gin::StringToSymbol(isolate, kLastErrorProperty);
280   // This CreateDataProperty() can fail, but there's nothing to do if it does
281   // (the exception will be caught by the TryCatch in SetError()).
282   ignore_result(secondary_parent->CreateDataProperty(
283       context, key,
284       gin::DataObjectBuilder(isolate).Set("message", error).Build()));
285 }
286 
287 }  // namespace extensions
288