Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(1002)

Side by Side Diff: chrome/browser/extensions/api/storage/managed_value_store_cache.cc

Issue 60823003: Introduced a ForwardingPolicyProvider. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: rebase Created 7 years, 1 month ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. 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 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 #include "chrome/browser/extensions/api/storage/managed_value_store_cache.h" 5 #include "chrome/browser/extensions/api/storage/managed_value_store_cache.h"
6 6
7 #include "base/bind.h" 7 #include "base/bind.h"
8 #include "base/bind_helpers.h" 8 #include "base/bind_helpers.h"
9 #include "base/callback.h" 9 #include "base/callback.h"
10 #include "base/file_util.h" 10 #include "base/file_util.h"
11 #include "base/logging.h" 11 #include "base/logging.h"
12 #include "base/memory/ref_counted.h" 12 #include "base/memory/weak_ptr.h"
13 #include "base/message_loop/message_loop_proxy.h"
14 #include "chrome/browser/chrome_notification_types.h" 13 #include "chrome/browser/chrome_notification_types.h"
15 #include "chrome/browser/extensions/api/storage/policy_value_store.h" 14 #include "chrome/browser/extensions/api/storage/policy_value_store.h"
16 #include "chrome/browser/extensions/api/storage/settings_storage_factory.h" 15 #include "chrome/browser/extensions/api/storage/settings_storage_factory.h"
17 #include "chrome/browser/extensions/extension_prefs.h" 16 #include "chrome/browser/extensions/extension_prefs.h"
18 #include "chrome/browser/extensions/extension_service.h" 17 #include "chrome/browser/extensions/extension_service.h"
19 #include "chrome/browser/extensions/extension_system.h" 18 #include "chrome/browser/extensions/extension_system.h"
20 #include "chrome/browser/policy/profile_policy_connector.h" 19 #include "chrome/browser/policy/profile_policy_connector.h"
21 #include "chrome/browser/policy/profile_policy_connector_factory.h" 20 #include "chrome/browser/policy/profile_policy_connector_factory.h"
21 #include "chrome/browser/policy/schema_map.h"
22 #include "chrome/browser/policy/schema_registry.h" 22 #include "chrome/browser/policy/schema_registry.h"
23 #include "chrome/browser/policy/schema_registry_service.h" 23 #include "chrome/browser/policy/schema_registry_service.h"
24 #include "chrome/browser/policy/schema_registry_service_factory.h" 24 #include "chrome/browser/policy/schema_registry_service_factory.h"
25 #include "chrome/browser/profiles/profile.h" 25 #include "chrome/browser/profiles/profile.h"
26 #include "chrome/browser/value_store/value_store_change.h" 26 #include "chrome/browser/value_store/value_store_change.h"
27 #include "chrome/common/extensions/api/storage.h" 27 #include "chrome/common/extensions/api/storage.h"
28 #include "chrome/common/extensions/api/storage/storage_schema_manifest_handler.h " 28 #include "chrome/common/extensions/api/storage/storage_schema_manifest_handler.h "
29 #include "chrome/common/extensions/extension.h" 29 #include "chrome/common/extensions/extension.h"
30 #include "chrome/common/extensions/extension_set.h" 30 #include "chrome/common/extensions/extension_set.h"
31 #include "components/policy/core/common/policy_namespace.h" 31 #include "components/policy/core/common/policy_namespace.h"
(...skipping 15 matching lines...) Expand all
47 namespace storage = api::storage; 47 namespace storage = api::storage;
48 48
49 namespace { 49 namespace {
50 50
51 const char kLoadSchemasBackgroundTaskTokenName[] = 51 const char kLoadSchemasBackgroundTaskTokenName[] =
52 "load_managed_storage_schemas_token"; 52 "load_managed_storage_schemas_token";
53 53
54 } // namespace 54 } // namespace
55 55
56 // This helper observes initialization of all the installed extensions and 56 // This helper observes initialization of all the installed extensions and
57 // subsequent loads and unloads, and keeps the PolicyService of the Profile 57 // subsequent loads and unloads, and keeps the SchemaRegistry of the Profile
58 // in sync with the current list of extensions. This allows the PolicyService 58 // in sync with the current list of extensions. This allows the PolicyService
59 // to fetch cloud policy for those extensions, and allows its providers to 59 // to fetch cloud policy for those extensions, and allows its providers to
60 // selectively load only extension policy that has users. 60 // selectively load only extension policy that has users.
61 class ManagedValueStoreCache::ExtensionTracker 61 class ManagedValueStoreCache::ExtensionTracker
62 : public content::NotificationObserver { 62 : public content::NotificationObserver {
63 public: 63 public:
64 explicit ExtensionTracker(Profile* profile); 64 explicit ExtensionTracker(Profile* profile);
65 virtual ~ExtensionTracker() {} 65 virtual ~ExtensionTracker() {}
66 66
67 // NotificationObserver implementation: 67 // NotificationObserver implementation:
(...skipping 74 matching lines...) Expand 10 before | Expand all | Expand 10 after
142 ExtensionSet::const_iterator it = added->begin(); 142 ExtensionSet::const_iterator it = added->begin();
143 while (it != added->end()) { 143 while (it != added->end()) {
144 std::string to_remove; 144 std::string to_remove;
145 if (!UsesManagedStorage(*it)) 145 if (!UsesManagedStorage(*it))
146 to_remove = (*it)->id(); 146 to_remove = (*it)->id();
147 ++it; 147 ++it;
148 if (!to_remove.empty()) 148 if (!to_remove.empty())
149 added->Remove(to_remove); 149 added->Remove(to_remove);
150 } 150 }
151 151
152 if (added->is_empty())
153 return;
154
155 // Load the schema files in a background thread. 152 // Load the schema files in a background thread.
156 BrowserThread::PostBlockingPoolSequencedTask( 153 BrowserThread::PostBlockingPoolSequencedTask(
157 kLoadSchemasBackgroundTaskTokenName, FROM_HERE, 154 kLoadSchemasBackgroundTaskTokenName, FROM_HERE,
158 base::Bind(&ExtensionTracker::LoadSchemas, 155 base::Bind(&ExtensionTracker::LoadSchemas,
159 base::Passed(&added), 156 base::Passed(&added),
160 weak_factory_.GetWeakPtr())); 157 weak_factory_.GetWeakPtr()));
161 } 158 }
162 159
163 bool ManagedValueStoreCache::ExtensionTracker::UsesManagedStorage( 160 bool ManagedValueStoreCache::ExtensionTracker::UsesManagedStorage(
164 const Extension* extension) const { 161 const Extension* extension) const {
(...skipping 39 matching lines...) Expand 10 before | Expand all | Expand 10 after
204 201
205 BrowserThread::PostTask(BrowserThread::UI, FROM_HERE, 202 BrowserThread::PostTask(BrowserThread::UI, FROM_HERE,
206 base::Bind(&ExtensionTracker::Register, self, 203 base::Bind(&ExtensionTracker::Register, self,
207 base::Owned(components.release()))); 204 base::Owned(components.release())));
208 } 205 }
209 206
210 void ManagedValueStoreCache::ExtensionTracker::Register( 207 void ManagedValueStoreCache::ExtensionTracker::Register(
211 const policy::ComponentMap* components) { 208 const policy::ComponentMap* components) {
212 schema_registry_->RegisterComponents(policy::POLICY_DOMAIN_EXTENSIONS, 209 schema_registry_->RegisterComponents(policy::POLICY_DOMAIN_EXTENSIONS,
213 *components); 210 *components);
211
212 // The first SetReady() call is performed after receiving
213 // NOTIFICATION_EXTENSIONS_READY, even if there are no managed extensions.
214 // It will trigger a loading of the initial policy for any managed
215 // extensions, and eventually the PolicyService will become ready for
216 // POLICY_DOMAIN_EXTENSIONS, and OnPolicyServiceInitialized() will be invoked.
217 // Subsequent calls to SetReady() are ignored.
218 schema_registry_->SetReady(policy::POLICY_DOMAIN_EXTENSIONS);
214 } 219 }
215 220
216 ManagedValueStoreCache::ManagedValueStoreCache( 221 ManagedValueStoreCache::ManagedValueStoreCache(
217 Profile* profile, 222 Profile* profile,
218 const scoped_refptr<SettingsStorageFactory>& factory, 223 const scoped_refptr<SettingsStorageFactory>& factory,
219 const scoped_refptr<SettingsObserverList>& observers) 224 const scoped_refptr<SettingsObserverList>& observers)
220 : weak_factory_(this), 225 : profile_(profile),
221 weak_this_on_ui_(weak_factory_.GetWeakPtr()), 226 policy_service_(policy::ProfilePolicyConnectorFactory::GetForProfile(
222 profile_(profile), 227 profile)->policy_service()),
223 event_router_(ExtensionSystem::Get(profile)->event_router()),
224 storage_factory_(factory), 228 storage_factory_(factory),
225 observers_(observers), 229 observers_(observers),
226 base_path_(profile->GetPath().AppendASCII( 230 base_path_(profile->GetPath().AppendASCII(
227 extensions::kManagedSettingsDirectoryName)) { 231 extensions::kManagedSettingsDirectoryName)) {
228 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 232 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
229 // |event_router_| can be NULL on unit_tests.
230 if (event_router_)
231 event_router_->RegisterObserver(this, storage::OnChanged::kEventName);
232 233
233 GetPolicyService()->AddObserver(policy::POLICY_DOMAIN_EXTENSIONS, this); 234 policy_service_->AddObserver(policy::POLICY_DOMAIN_EXTENSIONS, this);
234 235
235 extension_tracker_.reset(new ExtensionTracker(profile_)); 236 extension_tracker_.reset(new ExtensionTracker(profile_));
237
238 if (policy_service_->IsInitializationComplete(
239 policy::POLICY_DOMAIN_EXTENSIONS)) {
240 OnPolicyServiceInitialized(policy::POLICY_DOMAIN_EXTENSIONS);
241 }
236 } 242 }
237 243
238 ManagedValueStoreCache::~ManagedValueStoreCache() { 244 ManagedValueStoreCache::~ManagedValueStoreCache() {
239 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); 245 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
240 DCHECK(!event_router_);
241 // Delete the PolicyValueStores on FILE. 246 // Delete the PolicyValueStores on FILE.
242 store_map_.clear(); 247 store_map_.clear();
243 } 248 }
244 249
245 void ManagedValueStoreCache::ShutdownOnUI() { 250 void ManagedValueStoreCache::ShutdownOnUI() {
246 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 251 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
247 GetPolicyService()->RemoveObserver(policy::POLICY_DOMAIN_EXTENSIONS, this); 252 policy_service_->RemoveObserver(policy::POLICY_DOMAIN_EXTENSIONS, this);
248 if (event_router_)
249 event_router_->UnregisterObserver(this);
250 event_router_ = NULL;
251 weak_factory_.InvalidateWeakPtrs();
252 extension_tracker_.reset(); 253 extension_tracker_.reset();
253 } 254 }
254 255
255 void ManagedValueStoreCache::RunWithValueStoreForExtension( 256 void ManagedValueStoreCache::RunWithValueStoreForExtension(
256 const StorageCallback& callback, 257 const StorageCallback& callback,
257 scoped_refptr<const Extension> extension) { 258 scoped_refptr<const Extension> extension) {
258 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); 259 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
259 PolicyValueStore* store = GetStoreFor(extension->id()); 260 callback.Run(GetStoreFor(extension->id()));
260 if (store) {
261 callback.Run(store);
262 } else {
263 // First time that an extension calls storage.managed.get(). Create the
264 // store and load it with the current policy, and don't send event
265 // notifications.
266 CreateStoreFor(
267 extension->id(),
268 false,
269 base::Bind(&ManagedValueStoreCache::RunWithValueStoreForExtension,
270 base::Unretained(this),
271 callback,
272 extension));
273 }
274 } 261 }
275 262
276 void ManagedValueStoreCache::DeleteStorageSoon( 263 void ManagedValueStoreCache::DeleteStorageSoon(
277 const std::string& extension_id) { 264 const std::string& extension_id) {
278 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); 265 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
279 PolicyValueStore* store = GetStoreFor(extension_id); 266 // It's possible that the store exists, but hasn't been loaded yet
280 if (!store) { 267 // (because the extension is unloaded, for example). Open the database to
281 // It's possible that the store exists, but hasn't been loaded yet 268 // clear it if it exists.
282 // (because the extension is unloaded, for example). Open the database to 269 // TODO(joaodasilva): move this check to a ValueStore method.
283 // clear it if it exists. 270 if (!base::DirectoryExists(base_path_.AppendASCII(extension_id)))
284 // TODO(joaodasilva): move this check to a ValueStore method. 271 return;
285 if (base::DirectoryExists(base_path_.AppendASCII(extension_id))) { 272 GetStoreFor(extension_id)->DeleteStorage();
286 CreateStoreFor( 273 store_map_.erase(extension_id);
287 extension_id, 274 }
288 false, 275
289 base::Bind(&ManagedValueStoreCache::DeleteStorageSoon, 276 void ManagedValueStoreCache::OnPolicyServiceInitialized(
290 base::Unretained(this), 277 policy::PolicyDomain domain) {
not at google - send to devlin 2013/11/13 00:30:18 for consistency with this file: add a BrowserThrea
Joao da Silva 2013/11/13 08:56:47 Done.
291 extension_id)); 278 if (domain != policy::POLICY_DOMAIN_EXTENSIONS)
292 } 279 return;
293 } else { 280
294 store->DeleteStorage(); 281 // The PolicyService now has all the initial policies ready. Send policy
295 store_map_.erase(extension_id); 282 // for all the managed extensions to their backing stores now.
283 policy::SchemaRegistry* registry =
284 policy::SchemaRegistryServiceFactory::GetForContext(profile_);
285 const policy::ComponentMap* map = registry->schema_map()->GetComponents(
286 policy::POLICY_DOMAIN_EXTENSIONS);
287 if (!map)
288 return;
289
290 const policy::PolicyMap empty_map;
291 for (policy::ComponentMap::const_iterator it = map->begin();
292 it != map->end(); ++it) {
293 const policy::PolicyNamespace ns(policy::POLICY_DOMAIN_EXTENSIONS,
294 it->first);
295 // If there is no policy for |ns| then this will clear the previous store,
296 // if there is one.
297 OnPolicyUpdated(ns, empty_map, policy_service_->GetPolicies(ns));
not at google - send to devlin 2013/11/13 00:30:18 I don't know what a "component" is here, but just
Joao da Silva 2013/11/13 08:56:47 This for() loop is iterating over the list of exte
296 } 298 }
297 } 299 }
298 300
299 void ManagedValueStoreCache::OnPolicyUpdated(const policy::PolicyNamespace& ns, 301 void ManagedValueStoreCache::OnPolicyUpdated(const policy::PolicyNamespace& ns,
300 const policy::PolicyMap& previous, 302 const policy::PolicyMap& previous,
301 const policy::PolicyMap& current) { 303 const policy::PolicyMap& current) {
302 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 304 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
305
306 if (!policy_service_->IsInitializationComplete(
307 policy::POLICY_DOMAIN_EXTENSIONS)) {
not at google - send to devlin 2013/11/13 00:30:18 Why not check that ns != POLICY_DOMAIN_EXTENSIONS
Joao da Silva 2013/11/13 08:56:47 On line 234 this starts observing updates to POLIC
308 // OnPolicyUpdated is called whenever a policy changes, but it doesn't
309 // mean that all the policy providers are ready; wait until we get the
310 // final policy values before passing them to the store.
311 return;
312 }
313
303 BrowserThread::PostTask( 314 BrowserThread::PostTask(
304 BrowserThread::FILE, FROM_HERE, 315 BrowserThread::FILE, FROM_HERE,
305 base::Bind(&ManagedValueStoreCache::UpdatePolicyOnFILE, 316 base::Bind(&ManagedValueStoreCache::UpdatePolicyOnFILE,
306 base::Unretained(this), 317 base::Unretained(this),
307 ns.component_id, 318 ns.component_id,
308 base::Passed(current.DeepCopy()))); 319 base::Passed(current.DeepCopy())));
309 } 320 }
310 321
311 void ManagedValueStoreCache::UpdatePolicyOnFILE( 322 void ManagedValueStoreCache::UpdatePolicyOnFILE(
312 const std::string& extension_id, 323 const std::string& extension_id,
313 scoped_ptr<policy::PolicyMap> current_policy) { 324 scoped_ptr<policy::PolicyMap> current_policy) {
314 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); 325 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
315 PolicyValueStore* store = GetStoreFor(extension_id); 326 GetStoreFor(extension_id)->SetCurrentPolicy(*current_policy);
316 if (!store) {
317 // The extension hasn't executed any storage.managed.* calls, and isn't
318 // listening for onChanged() either. Ignore this notification in that case.
319 return;
320 }
321 // Update the policy on the backing store, and fire notifications if it
322 // changed.
323 store->SetCurrentPolicy(*current_policy, true);
324 }
325
326 void ManagedValueStoreCache::OnListenerAdded(
327 const EventListenerInfo& details) {
328 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
329 DCHECK_EQ(std::string(storage::OnChanged::kEventName), details.event_name);
330 // This is invoked on several occasions:
331 //
332 // 1. when an extension first registers to observe storage.onChanged; in this
333 // case the backend doesn't have any previous data persisted, and it won't
334 // trigger a notification.
335 //
336 // 2. when the browser starts up and all existing extensions re-register for
337 // the onChanged event. In this case, if the current policy differs from
338 // the persisted version then a notification will be sent.
339 //
340 // 3. a policy update just occurred and sent a notification, and an extension
341 // with EventPages that is observing onChanged just woke up and registed
342 // again. In this case the policy update already persisted the current
343 // policy version, and |store| already exists.
344 BrowserThread::PostTask(
345 BrowserThread::FILE, FROM_HERE,
346 base::Bind(&ManagedValueStoreCache::CreateForExtensionOnFILE,
347 base::Unretained(this),
348 details.extension_id));
349 }
350
351 void ManagedValueStoreCache::CreateForExtensionOnFILE(
352 const std::string& extension_id) {
353 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
354 PolicyValueStore* store = GetStoreFor(extension_id);
355 if (!store)
356 CreateStoreFor(extension_id, true, base::Closure());
357 } 327 }
358 328
359 PolicyValueStore* ManagedValueStoreCache::GetStoreFor( 329 PolicyValueStore* ManagedValueStoreCache::GetStoreFor(
360 const std::string& extension_id) { 330 const std::string& extension_id) {
361 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); 331 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
332
362 PolicyValueStoreMap::iterator it = store_map_.find(extension_id); 333 PolicyValueStoreMap::iterator it = store_map_.find(extension_id);
363 if (it == store_map_.end()) 334 if (it != store_map_.end())
364 return NULL; 335 return it->second.get();
365 return it->second.get();
366 }
367 336
368 void ManagedValueStoreCache::CreateStoreFor( 337 // Create the store now, and serve the cached policy until the PolicyService
369 const std::string& extension_id, 338 // sends updated values.
370 bool notify_if_changed, 339 PolicyValueStore* store = new PolicyValueStore(
371 const base::Closure& continuation) { 340 extension_id,
372 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); 341 observers_,
373 DCHECK(!GetStoreFor(extension_id)); 342 make_scoped_ptr(storage_factory_->Create(base_path_, extension_id)));
374 // Creating or loading an existing database requires an immediate update 343 store_map_[extension_id] = make_linked_ptr(store);
375 // with the current policy for the corresponding extension, which must be
376 // retrieved on UI.
377 BrowserThread::PostTask(
378 BrowserThread::UI, FROM_HERE,
379 base::Bind(&ManagedValueStoreCache::GetInitialPolicy,
380 weak_this_on_ui_,
381 extension_id,
382 notify_if_changed,
383 continuation));
384 }
385 344
386 void ManagedValueStoreCache::GetInitialPolicy( 345 return store;
387 const std::string& extension_id,
388 bool notify_if_changed,
389 const base::Closure& continuation) {
390 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
391
392 policy::PolicyService* policy_service = GetPolicyService();
393
394 // If initialization of POLICY_DOMAIN_EXTENSIONS isn't complete then all the
395 // policies served are empty; let the extension see what's cached in LevelDB
396 // in that case. The PolicyService will issue notifications once new policies
397 // are ready.
398 scoped_ptr<policy::PolicyMap> policy;
399 if (policy_service->IsInitializationComplete(
400 policy::POLICY_DOMAIN_EXTENSIONS)) {
401 policy::PolicyNamespace ns(policy::POLICY_DOMAIN_EXTENSIONS, extension_id);
402 policy = policy_service->GetPolicies(ns).DeepCopy();
403 }
404
405 // Now post back to FILE to create the database.
406 BrowserThread::PostTask(
407 BrowserThread::FILE, FROM_HERE,
408 base::Bind(&ManagedValueStoreCache::CreateStoreWithInitialPolicy,
409 base::Unretained(this),
410 extension_id,
411 notify_if_changed,
412 base::Passed(&policy),
413 continuation));
414 }
415
416 void ManagedValueStoreCache::CreateStoreWithInitialPolicy(
417 const std::string& extension_id,
418 bool notify_if_changed,
419 scoped_ptr<policy::PolicyMap> initial_policy,
420 const base::Closure& continuation) {
421 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
422 // If a 2nd call to CreateStoreFor() is issued before the 1st gets to execute
423 // its UI task, then the 2nd will enter this function but the store has
424 // already been created. Check for that.
425 PolicyValueStore* store = GetStoreFor(extension_id);
426
427 if (!store) {
428 // Create it now.
429
430 // If the database doesn't exist yet then this is the initial install,
431 // and no notifications should be issued in that case.
432 // TODO(joaodasilva): move this check to a ValueStore method.
433 if (!base::DirectoryExists(base_path_.AppendASCII(extension_id)))
434 notify_if_changed = false;
435
436 store = new PolicyValueStore(
437 extension_id,
438 observers_,
439 make_scoped_ptr(storage_factory_->Create(base_path_, extension_id)));
440 store_map_[extension_id] = make_linked_ptr(store);
441 }
442
443 // Send the latest policy to the store, if it's already available.
444 if (initial_policy)
445 store->SetCurrentPolicy(*initial_policy, notify_if_changed);
446
447 // And finally resume from where this process started.
448 if (!continuation.is_null())
449 continuation.Run();
450 }
451
452 policy::PolicyService* ManagedValueStoreCache::GetPolicyService() {
453 policy::ProfilePolicyConnector* connector =
454 policy::ProfilePolicyConnectorFactory::GetForProfile(profile_);
455 return connector->policy_service();
456 } 346 }
457 347
458 } // namespace extensions 348 } // namespace extensions
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698