extension_context_menu_model_unittest.cc revision 6e8cce623b6e4fe0c9e4af605d675dd9d0338c38
15821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)// Copyright (c) 2012 The Chromium Authors. All rights reserved. 25821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)// Use of this source code is governed by a BSD-style license that can be 35821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)// found in the LICENSE file. 45821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 55821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)#include "chrome/browser/extensions/extension_context_menu_model.h" 65821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 7f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)#include "chrome/browser/extensions/extension_service.h" 8f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)#include "chrome/browser/extensions/extension_service_test_base.h" 96e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)#include "chrome/browser/extensions/menu_manager.h" 106e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)#include "chrome/browser/extensions/menu_manager_factory.h" 115821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)#include "chrome/browser/ui/browser.h" 122a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)#include "chrome/browser/ui/host_desktop.h" 136e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)#include "chrome/common/extensions/api/context_menus.h" 145821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)#include "chrome/test/base/test_browser_window.h" 155821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)#include "chrome/test/base/testing_profile.h" 165d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)#include "extensions/browser/extension_system.h" 17f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles)#include "extensions/browser/test_management_policy.h" 18f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles)#include "extensions/common/extension_builder.h" 19f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles)#include "extensions/common/value_builder.h" 205821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)#include "testing/gtest/include/gtest/gtest.h" 215821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 225821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)namespace extensions { 235821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 245821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)class ExtensionContextMenuModelTest : public ExtensionServiceTestBase { 256e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) public: 266e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) ExtensionContextMenuModelTest(); 276e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) 286e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) // Build an extension to pass to the menu constructor. It needs an 296e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) // ExtensionAction. 306e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) scoped_refptr<Extension> BuildExtension(); 316e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) 326e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) // Creates an extension menu item for |extension| with the given |context| 336e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) // and adds it to |manager|. Refreshes |model| to show new item. 346e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) void AddContextItemAndRefreshModel(MenuManager* manager, 356e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) Extension* extension, 366e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) MenuItem::Context context, 376e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) ExtensionContextMenuModel* model); 386e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) 396e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) // Reinitializes the given |model|. 406e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) void RefreshMenu(ExtensionContextMenuModel* model); 416e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) 426e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) // Returns the number of extension menu items that show up in |model|. 436e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) int CountExtensionItems(ExtensionContextMenuModel* model); 446e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) 456e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) private: 466e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) int cur_id_; 475821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)}; 485821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 496e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)ExtensionContextMenuModelTest::ExtensionContextMenuModelTest() : cur_id_(0) { 506e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)} 516e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) 526e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)scoped_refptr<Extension> ExtensionContextMenuModelTest::BuildExtension() { 536e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) return ExtensionBuilder() 546e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) .SetManifest(DictionaryBuilder() 556e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) .Set("name", "Page Action Extension") 566e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) .Set("version", "1") 576e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) .Set("manifest_version", 2) 586e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) .Set("page_action", 596e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) DictionaryBuilder().Set("default_title", "Hello"))) 606e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) .Build(); 616e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)} 626e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) 636e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)void ExtensionContextMenuModelTest::AddContextItemAndRefreshModel( 646e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) MenuManager* manager, 656e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) Extension* extension, 666e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) MenuItem::Context context, 676e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) ExtensionContextMenuModel* model) { 686e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) MenuItem::Type type = MenuItem::NORMAL; 696e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) MenuItem::ContextList contexts(context); 706e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) const MenuItem::ExtensionKey key(extension->id()); 716e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) MenuItem::Id id(false, key); 726e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) id.uid = ++cur_id_; 736e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) manager->AddContextItem(extension, 746e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) new MenuItem(id, 756e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) "test", 766e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) false, // checked 776e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) true, // enabled 786e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) type, 796e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) contexts)); 806e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) RefreshMenu(model); 816e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)} 826e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) 836e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)void ExtensionContextMenuModelTest::RefreshMenu( 846e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) ExtensionContextMenuModel* model) { 856e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) model->InitMenu(model->GetExtension()); 866e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)} 876e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) 886e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)int ExtensionContextMenuModelTest::CountExtensionItems( 896e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) ExtensionContextMenuModel* model) { 906e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) return model->extension_items_count_; 916e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)} 926e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) 936e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)namespace { 946e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) 955821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)// Tests that applicable menu items are disabled when a ManagementPolicy 965821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)// prohibits them. 975821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)TEST_F(ExtensionContextMenuModelTest, PolicyDisablesItems) { 985821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) InitializeEmptyExtensionService(); 996e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) scoped_refptr<Extension> extension = BuildExtension(); 100868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles) ASSERT_TRUE(extension.get()); 101868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles) service_->AddExtension(extension.get()); 1025821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 1035821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) // Create a Browser for the ExtensionContextMenuModel to use. 104eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch Browser::CreateParams params(profile_.get(), chrome::GetActiveDesktop()); 1055821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) TestBrowserWindow test_window; 1065821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) params.window = &test_window; 1075821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) Browser browser(params); 1085821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 1095821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) scoped_refptr<ExtensionContextMenuModel> menu( 1106e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) new ExtensionContextMenuModel(extension.get(), &browser)); 1115821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 1125821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) extensions::ExtensionSystem* system = 1135821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) extensions::ExtensionSystem::Get(profile_.get()); 1145821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) system->management_policy()->UnregisterAllProviders(); 1155821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 1165821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) // Actions should be enabled. 1175821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) ASSERT_TRUE(menu->IsCommandIdEnabled(ExtensionContextMenuModel::UNINSTALL)); 1185821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 1195821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) extensions::TestManagementPolicyProvider policy_provider( 1205821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) extensions::TestManagementPolicyProvider::PROHIBIT_MODIFY_STATUS); 1215821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) system->management_policy()->RegisterProvider(&policy_provider); 1225821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 1235821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) // Now the actions are disabled. 1245821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) ASSERT_FALSE(menu->IsCommandIdEnabled(ExtensionContextMenuModel::UNINSTALL)); 1252a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 1262a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // Don't leave |policy_provider| dangling. 1272a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) system->management_policy()->UnregisterAllProviders(); 1285821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)} 1295821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 1306e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)TEST_F(ExtensionContextMenuModelTest, ExtensionItemTest) { 1316e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) InitializeEmptyExtensionService(); 1326e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) scoped_refptr<Extension> extension = BuildExtension(); 1336e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) ASSERT_TRUE(extension.get()); 1346e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) service_->AddExtension(extension.get()); 1356e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) 1366e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) // Create a Browser for the ExtensionContextMenuModel to use. 1376e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) Browser::CreateParams params(profile_.get(), chrome::GetActiveDesktop()); 1386e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) TestBrowserWindow test_window; 1396e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) params.window = &test_window; 1406e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) Browser browser(params); 1416e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) 1426e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) // Create a MenuManager for adding context items. 1436e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) MenuManager* manager = static_cast<MenuManager*>( 1446e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) (MenuManagerFactory::GetInstance()->SetTestingFactoryAndUse( 1456e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) profile_.get(), 1466e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) &MenuManagerFactory::BuildServiceInstanceForTesting))); 1476e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) ASSERT_TRUE(manager); 1486e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) 1496e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) scoped_refptr<ExtensionContextMenuModel> menu( 1506e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) new ExtensionContextMenuModel(extension.get(), &browser)); 1516e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) 1526e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) // There should be no extension items yet. 1536e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) EXPECT_EQ(0, CountExtensionItems(menu)); 1546e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) 1556e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) // Add a browser action menu item for |extension| to |manager|. 1566e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) AddContextItemAndRefreshModel( 1576e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) manager, extension.get(), MenuItem::BROWSER_ACTION, menu); 1586e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) 1596e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) // Since |extension| has a page action, the browser action menu item should 1606e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) // not be present. 1616e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) EXPECT_EQ(0, CountExtensionItems(menu)); 1626e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) 1636e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) // Add a page action menu item and reset the context menu. 1646e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) AddContextItemAndRefreshModel( 1656e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) manager, extension.get(), MenuItem::PAGE_ACTION, menu); 1666e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) 1676e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) // The page action item should be present because |extension| has a page 1686e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) // action. 1696e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) EXPECT_EQ(1, CountExtensionItems(menu)); 1706e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) 1716e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) // Create more page action items to test top level menu item limitations. 1726e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) for (int i = 0; i < api::context_menus::ACTION_MENU_TOP_LEVEL_LIMIT; ++i) 1736e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) AddContextItemAndRefreshModel( 1746e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) manager, extension.get(), MenuItem::PAGE_ACTION, menu); 1756e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) 1766e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) // The menu should only have a limited number of extension items, since they 1776e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) // are all top level items, and we limit the number of top level extension 1786e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) // items. 1796e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) EXPECT_EQ(api::context_menus::ACTION_MENU_TOP_LEVEL_LIMIT, 1806e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) CountExtensionItems(menu)); 1816e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) 1826e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) AddContextItemAndRefreshModel( 1836e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) manager, extension.get(), MenuItem::PAGE_ACTION, menu); 1846e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) 1856e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) // Adding another top level item should not increase the count. 1866e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) EXPECT_EQ(api::context_menus::ACTION_MENU_TOP_LEVEL_LIMIT, 1876e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) CountExtensionItems(menu)); 1886e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)} 1896e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) 1905821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)} // namespace 1915821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)} // namespace extensions 192