OLD | NEW |
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/crx_installer.h" | 5 #include "chrome/browser/extensions/crx_installer.h" |
6 | 6 |
7 #include <stddef.h> | 7 #include <stddef.h> |
| 8 |
8 #include <utility> | 9 #include <utility> |
9 | 10 |
10 #include "base/at_exit.h" | 11 #include "base/at_exit.h" |
11 #include "base/files/file_path.h" | 12 #include "base/files/file_path.h" |
12 #include "base/macros.h" | 13 #include "base/macros.h" |
| 14 #include "base/memory/ptr_util.h" |
13 #include "base/memory/ref_counted.h" | 15 #include "base/memory/ref_counted.h" |
14 #include "base/strings/utf_string_conversions.h" | 16 #include "base/strings/utf_string_conversions.h" |
15 #include "build/build_config.h" | 17 #include "build/build_config.h" |
16 #include "chrome/browser/download/download_crx_util.h" | 18 #include "chrome/browser/download/download_crx_util.h" |
17 #include "chrome/browser/extensions/browser_action_test_util.h" | 19 #include "chrome/browser/extensions/browser_action_test_util.h" |
18 #include "chrome/browser/extensions/extension_browsertest.h" | 20 #include "chrome/browser/extensions/extension_browsertest.h" |
19 #include "chrome/browser/extensions/extension_install_prompt.h" | 21 #include "chrome/browser/extensions/extension_install_prompt.h" |
20 #include "chrome/browser/extensions/extension_install_prompt_show_params.h" | 22 #include "chrome/browser/extensions/extension_install_prompt_show_params.h" |
21 #include "chrome/browser/extensions/extension_service.h" | 23 #include "chrome/browser/extensions/extension_service.h" |
22 #include "chrome/browser/extensions/extension_tab_util.h" | 24 #include "chrome/browser/extensions/extension_tab_util.h" |
(...skipping 66 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
89 const std::string& extension_id() { return extension_id_; } | 91 const std::string& extension_id() { return extension_id_; } |
90 bool confirmation_requested() const { return confirmation_requested_; } | 92 bool confirmation_requested() const { return confirmation_requested_; } |
91 const base::string16& error() const { return error_; } | 93 const base::string16& error() const { return error_; } |
92 | 94 |
93 void set_extension_id(const std::string& id) { extension_id_ = id; } | 95 void set_extension_id(const std::string& id) { extension_id_ = id; } |
94 void set_confirmation_requested(bool requested) { | 96 void set_confirmation_requested(bool requested) { |
95 confirmation_requested_ = requested; | 97 confirmation_requested_ = requested; |
96 } | 98 } |
97 void set_error(const base::string16& error) { error_ = error; } | 99 void set_error(const base::string16& error) { error_ = error; } |
98 | 100 |
99 scoped_ptr<ExtensionInstallPrompt> CreatePrompt(); | 101 std::unique_ptr<ExtensionInstallPrompt> CreatePrompt(); |
100 | 102 |
101 private: | 103 private: |
102 | 104 |
103 // Data used to create a prompt. | 105 // Data used to create a prompt. |
104 content::WebContents* web_contents_; | 106 content::WebContents* web_contents_; |
105 | 107 |
106 // Data reported back to us by the prompt we created. | 108 // Data reported back to us by the prompt we created. |
107 bool confirmation_requested_; | 109 bool confirmation_requested_; |
108 std::string extension_id_; | 110 std::string extension_id_; |
109 base::string16 error_; | 111 base::string16 error_; |
110 | 112 |
111 scoped_ptr<ScopedTestDialogAutoConfirm> auto_confirm; | 113 std::unique_ptr<ScopedTestDialogAutoConfirm> auto_confirm; |
112 | 114 |
113 DISALLOW_COPY_AND_ASSIGN(MockPromptProxy); | 115 DISALLOW_COPY_AND_ASSIGN(MockPromptProxy); |
114 }; | 116 }; |
115 | 117 |
116 SkBitmap CreateSquareBitmap(int size) { | 118 SkBitmap CreateSquareBitmap(int size) { |
117 SkBitmap bitmap; | 119 SkBitmap bitmap; |
118 bitmap.allocN32Pixels(size, size); | 120 bitmap.allocN32Pixels(size, size); |
119 bitmap.eraseColor(SK_ColorRED); | 121 bitmap.eraseColor(SK_ColorRED); |
120 return bitmap; | 122 return bitmap; |
121 } | 123 } |
(...skipping 47 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
169 | 171 |
170 MockPromptProxy::MockPromptProxy(content::WebContents* web_contents) | 172 MockPromptProxy::MockPromptProxy(content::WebContents* web_contents) |
171 : web_contents_(web_contents), | 173 : web_contents_(web_contents), |
172 confirmation_requested_(false), | 174 confirmation_requested_(false), |
173 auto_confirm(new ScopedTestDialogAutoConfirm( | 175 auto_confirm(new ScopedTestDialogAutoConfirm( |
174 ScopedTestDialogAutoConfirm::ACCEPT)) { | 176 ScopedTestDialogAutoConfirm::ACCEPT)) { |
175 } | 177 } |
176 | 178 |
177 MockPromptProxy::~MockPromptProxy() {} | 179 MockPromptProxy::~MockPromptProxy() {} |
178 | 180 |
179 scoped_ptr<ExtensionInstallPrompt> MockPromptProxy::CreatePrompt() { | 181 std::unique_ptr<ExtensionInstallPrompt> MockPromptProxy::CreatePrompt() { |
180 return scoped_ptr<MockInstallPrompt>( | 182 return std::unique_ptr<MockInstallPrompt>( |
181 new MockInstallPrompt(web_contents_, this)); | 183 new MockInstallPrompt(web_contents_, this)); |
182 } | 184 } |
183 | 185 |
184 | 186 std::unique_ptr<MockPromptProxy> CreateMockPromptProxyForBrowser( |
185 scoped_ptr<MockPromptProxy> CreateMockPromptProxyForBrowser( | |
186 Browser* browser) { | 187 Browser* browser) { |
187 return make_scoped_ptr(new MockPromptProxy( | 188 return base::WrapUnique( |
188 browser->tab_strip_model()->GetActiveWebContents())); | 189 new MockPromptProxy(browser->tab_strip_model()->GetActiveWebContents())); |
189 } | 190 } |
190 | 191 |
191 class ManagementPolicyMock : public extensions::ManagementPolicy::Provider { | 192 class ManagementPolicyMock : public extensions::ManagementPolicy::Provider { |
192 public: | 193 public: |
193 ManagementPolicyMock() {} | 194 ManagementPolicyMock() {} |
194 | 195 |
195 std::string GetDebugPolicyProviderName() const override { | 196 std::string GetDebugPolicyProviderName() const override { |
196 return "ManagementPolicyMock"; | 197 return "ManagementPolicyMock"; |
197 } | 198 } |
198 | 199 |
199 bool UserMayLoad(const Extension* extension, | 200 bool UserMayLoad(const Extension* extension, |
200 base::string16* error) const override { | 201 base::string16* error) const override { |
201 *error = base::UTF8ToUTF16("Dummy error message"); | 202 *error = base::UTF8ToUTF16("Dummy error message"); |
202 return false; | 203 return false; |
203 } | 204 } |
204 }; | 205 }; |
205 | 206 |
206 } // namespace | 207 } // namespace |
207 | 208 |
208 class ExtensionCrxInstallerTest : public ExtensionBrowserTest { | 209 class ExtensionCrxInstallerTest : public ExtensionBrowserTest { |
209 protected: | 210 protected: |
210 scoped_ptr<WebstoreInstaller::Approval> GetApproval( | 211 std::unique_ptr<WebstoreInstaller::Approval> GetApproval( |
211 const char* manifest_dir, | 212 const char* manifest_dir, |
212 const std::string& id, | 213 const std::string& id, |
213 bool strict_manifest_checks) { | 214 bool strict_manifest_checks) { |
214 scoped_ptr<WebstoreInstaller::Approval> result; | 215 std::unique_ptr<WebstoreInstaller::Approval> result; |
215 | 216 |
216 base::FilePath ext_path = test_data_dir_.AppendASCII(manifest_dir); | 217 base::FilePath ext_path = test_data_dir_.AppendASCII(manifest_dir); |
217 std::string error; | 218 std::string error; |
218 scoped_ptr<base::DictionaryValue> parsed_manifest( | 219 std::unique_ptr<base::DictionaryValue> parsed_manifest( |
219 file_util::LoadManifest(ext_path, &error)); | 220 file_util::LoadManifest(ext_path, &error)); |
220 if (!parsed_manifest.get() || !error.empty()) | 221 if (!parsed_manifest.get() || !error.empty()) |
221 return result; | 222 return result; |
222 | 223 |
223 return WebstoreInstaller::Approval::CreateWithNoInstallPrompt( | 224 return WebstoreInstaller::Approval::CreateWithNoInstallPrompt( |
224 browser()->profile(), id, std::move(parsed_manifest), | 225 browser()->profile(), id, std::move(parsed_manifest), |
225 strict_manifest_checks); | 226 strict_manifest_checks); |
226 } | 227 } |
227 | 228 |
228 void RunCrxInstaller(const WebstoreInstaller::Approval* approval, | 229 void RunCrxInstaller(const WebstoreInstaller::Approval* approval, |
229 scoped_ptr<ExtensionInstallPrompt> prompt, | 230 std::unique_ptr<ExtensionInstallPrompt> prompt, |
230 const base::FilePath& crx_path) { | 231 const base::FilePath& crx_path) { |
231 ExtensionService* service = extensions::ExtensionSystem::Get( | 232 ExtensionService* service = extensions::ExtensionSystem::Get( |
232 browser()->profile())->extension_service(); | 233 browser()->profile())->extension_service(); |
233 scoped_refptr<CrxInstaller> installer( | 234 scoped_refptr<CrxInstaller> installer( |
234 CrxInstaller::Create(service, std::move(prompt), approval)); | 235 CrxInstaller::Create(service, std::move(prompt), approval)); |
235 installer->set_allow_silent_install(true); | 236 installer->set_allow_silent_install(true); |
236 installer->set_is_gallery_install(true); | 237 installer->set_is_gallery_install(true); |
237 installer->InstallCrx(crx_path); | 238 installer->InstallCrx(crx_path); |
238 content::RunMessageLoop(); | 239 content::RunMessageLoop(); |
239 } | 240 } |
240 | 241 |
241 // Installs a crx from |crx_relpath| (a path relative to the extension test | 242 // Installs a crx from |crx_relpath| (a path relative to the extension test |
242 // data dir) with expected id |id|. | 243 // data dir) with expected id |id|. |
243 void InstallWithPrompt(const char* ext_relpath, | 244 void InstallWithPrompt(const char* ext_relpath, |
244 const std::string& id, | 245 const std::string& id, |
245 MockPromptProxy* mock_install_prompt) { | 246 MockPromptProxy* mock_install_prompt) { |
246 base::FilePath ext_path = test_data_dir_.AppendASCII(ext_relpath); | 247 base::FilePath ext_path = test_data_dir_.AppendASCII(ext_relpath); |
247 | 248 |
248 scoped_ptr<WebstoreInstaller::Approval> approval; | 249 std::unique_ptr<WebstoreInstaller::Approval> approval; |
249 if (!id.empty()) | 250 if (!id.empty()) |
250 approval = GetApproval(ext_relpath, id, true); | 251 approval = GetApproval(ext_relpath, id, true); |
251 | 252 |
252 base::FilePath crx_path = PackExtension(ext_path); | 253 base::FilePath crx_path = PackExtension(ext_path); |
253 EXPECT_FALSE(crx_path.empty()); | 254 EXPECT_FALSE(crx_path.empty()); |
254 RunCrxInstaller(approval.get(), mock_install_prompt->CreatePrompt(), | 255 RunCrxInstaller(approval.get(), mock_install_prompt->CreatePrompt(), |
255 crx_path); | 256 crx_path); |
256 | 257 |
257 EXPECT_TRUE(mock_install_prompt->did_succeed()); | 258 EXPECT_TRUE(mock_install_prompt->did_succeed()); |
258 } | 259 } |
259 | 260 |
260 // Installs an extension and checks that it has scopes granted IFF | 261 // Installs an extension and checks that it has scopes granted IFF |
261 // |record_oauth2_grant| is true. | 262 // |record_oauth2_grant| is true. |
262 void CheckHasEmptyScopesAfterInstall(const std::string& ext_relpath, | 263 void CheckHasEmptyScopesAfterInstall(const std::string& ext_relpath, |
263 bool record_oauth2_grant) { | 264 bool record_oauth2_grant) { |
264 scoped_ptr<MockPromptProxy> mock_prompt = | 265 std::unique_ptr<MockPromptProxy> mock_prompt = |
265 CreateMockPromptProxyForBrowser(browser()); | 266 CreateMockPromptProxyForBrowser(browser()); |
266 | 267 |
267 InstallWithPrompt("browsertest/scopes", std::string(), mock_prompt.get()); | 268 InstallWithPrompt("browsertest/scopes", std::string(), mock_prompt.get()); |
268 | 269 |
269 scoped_ptr<const PermissionSet> permissions = | 270 std::unique_ptr<const PermissionSet> permissions = |
270 ExtensionPrefs::Get(browser()->profile()) | 271 ExtensionPrefs::Get(browser()->profile()) |
271 ->GetGrantedPermissions(mock_prompt->extension_id()); | 272 ->GetGrantedPermissions(mock_prompt->extension_id()); |
272 ASSERT_TRUE(permissions.get()); | 273 ASSERT_TRUE(permissions.get()); |
273 } | 274 } |
274 | 275 |
275 void InstallWebAppAndVerifyNoErrors() { | 276 void InstallWebAppAndVerifyNoErrors() { |
276 ExtensionService* service = | 277 ExtensionService* service = |
277 extensions::ExtensionSystem::Get(browser()->profile()) | 278 extensions::ExtensionSystem::Get(browser()->profile()) |
278 ->extension_service(); | 279 ->extension_service(); |
279 scoped_refptr<CrxInstaller> crx_installer( | 280 scoped_refptr<CrxInstaller> crx_installer( |
(...skipping 70 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
350 | 351 |
351 const int kNumDownloadsExpected = 1; | 352 const int kNumDownloadsExpected = 1; |
352 | 353 |
353 LOG(ERROR) << "PackAndInstallExtension: Packing extension"; | 354 LOG(ERROR) << "PackAndInstallExtension: Packing extension"; |
354 base::FilePath crx_path = PackExtension( | 355 base::FilePath crx_path = PackExtension( |
355 test_data_dir_.AppendASCII("common/background_page")); | 356 test_data_dir_.AppendASCII("common/background_page")); |
356 ASSERT_FALSE(crx_path.empty()); | 357 ASSERT_FALSE(crx_path.empty()); |
357 std::string crx_path_string(crx_path.value().begin(), crx_path.value().end()); | 358 std::string crx_path_string(crx_path.value().begin(), crx_path.value().end()); |
358 GURL url = GURL(std::string("file:///").append(crx_path_string)); | 359 GURL url = GURL(std::string("file:///").append(crx_path_string)); |
359 | 360 |
360 scoped_ptr<MockPromptProxy> mock_prompt = | 361 std::unique_ptr<MockPromptProxy> mock_prompt = |
361 CreateMockPromptProxyForBrowser(browser()); | 362 CreateMockPromptProxyForBrowser(browser()); |
362 download_crx_util::SetMockInstallPromptForTesting( | 363 download_crx_util::SetMockInstallPromptForTesting( |
363 mock_prompt->CreatePrompt()); | 364 mock_prompt->CreatePrompt()); |
364 | 365 |
365 LOG(ERROR) << "PackAndInstallExtension: Getting download manager"; | 366 LOG(ERROR) << "PackAndInstallExtension: Getting download manager"; |
366 content::DownloadManager* download_manager = | 367 content::DownloadManager* download_manager = |
367 content::BrowserContext::GetDownloadManager(browser()->profile()); | 368 content::BrowserContext::GetDownloadManager(browser()->profile()); |
368 | 369 |
369 LOG(ERROR) << "PackAndInstallExtension: Setting observer"; | 370 LOG(ERROR) << "PackAndInstallExtension: Setting observer"; |
370 scoped_ptr<content::DownloadTestObserver> observer( | 371 std::unique_ptr<content::DownloadTestObserver> observer( |
371 new content::DownloadTestObserverTerminal( | 372 new content::DownloadTestObserverTerminal( |
372 download_manager, kNumDownloadsExpected, | 373 download_manager, kNumDownloadsExpected, |
373 content::DownloadTestObserver::ON_DANGEROUS_DOWNLOAD_ACCEPT)); | 374 content::DownloadTestObserver::ON_DANGEROUS_DOWNLOAD_ACCEPT)); |
374 LOG(ERROR) << "PackAndInstallExtension: Navigating to URL"; | 375 LOG(ERROR) << "PackAndInstallExtension: Navigating to URL"; |
375 ui_test_utils::NavigateToURLWithDisposition(browser(), url, CURRENT_TAB, | 376 ui_test_utils::NavigateToURLWithDisposition(browser(), url, CURRENT_TAB, |
376 ui_test_utils::BROWSER_TEST_NONE); | 377 ui_test_utils::BROWSER_TEST_NONE); |
377 | 378 |
378 EXPECT_TRUE(WaitForCrxInstallerDone()); | 379 EXPECT_TRUE(WaitForCrxInstallerDone()); |
379 LOG(ERROR) << "PackAndInstallExtension: Extension install"; | 380 LOG(ERROR) << "PackAndInstallExtension: Extension install"; |
380 EXPECT_TRUE(mock_prompt->confirmation_requested()); | 381 EXPECT_TRUE(mock_prompt->confirmation_requested()); |
(...skipping 18 matching lines...) Expand all Loading... |
399 EXPECT_NO_FATAL_FAILURE(CheckHasEmptyScopesAfterInstall("browsertest/scopes", | 400 EXPECT_NO_FATAL_FAILURE(CheckHasEmptyScopesAfterInstall("browsertest/scopes", |
400 false)); | 401 false)); |
401 } | 402 } |
402 | 403 |
403 IN_PROC_BROWSER_TEST_F(ExtensionCrxInstallerTest, AllowOffStore) { | 404 IN_PROC_BROWSER_TEST_F(ExtensionCrxInstallerTest, AllowOffStore) { |
404 ExtensionService* service = extensions::ExtensionSystem::Get( | 405 ExtensionService* service = extensions::ExtensionSystem::Get( |
405 browser()->profile())->extension_service(); | 406 browser()->profile())->extension_service(); |
406 const bool kTestData[] = {false, true}; | 407 const bool kTestData[] = {false, true}; |
407 | 408 |
408 for (size_t i = 0; i < arraysize(kTestData); ++i) { | 409 for (size_t i = 0; i < arraysize(kTestData); ++i) { |
409 scoped_ptr<MockPromptProxy> mock_prompt = | 410 std::unique_ptr<MockPromptProxy> mock_prompt = |
410 CreateMockPromptProxyForBrowser(browser()); | 411 CreateMockPromptProxyForBrowser(browser()); |
411 | 412 |
412 scoped_refptr<CrxInstaller> crx_installer( | 413 scoped_refptr<CrxInstaller> crx_installer( |
413 CrxInstaller::Create(service, mock_prompt->CreatePrompt())); | 414 CrxInstaller::Create(service, mock_prompt->CreatePrompt())); |
414 crx_installer->set_install_cause( | 415 crx_installer->set_install_cause( |
415 extension_misc::INSTALL_CAUSE_USER_DOWNLOAD); | 416 extension_misc::INSTALL_CAUSE_USER_DOWNLOAD); |
416 | 417 |
417 if (kTestData[i]) { | 418 if (kTestData[i]) { |
418 crx_installer->set_off_store_install_allow_reason( | 419 crx_installer->set_off_store_install_allow_reason( |
419 CrxInstaller::OffStoreInstallAllowedInTest); | 420 CrxInstaller::OffStoreInstallAllowedInTest); |
(...skipping 103 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
523 | 524 |
524 blacklist_db->SetUnsafe("gllekhaobjnhgeagipipnkpmmmpchacm"); | 525 blacklist_db->SetUnsafe("gllekhaobjnhgeagipipnkpmmmpchacm"); |
525 | 526 |
526 base::FilePath crx_path = test_data_dir_.AppendASCII("theme_hidpi_crx") | 527 base::FilePath crx_path = test_data_dir_.AppendASCII("theme_hidpi_crx") |
527 .AppendASCII("theme_hidpi.crx"); | 528 .AppendASCII("theme_hidpi.crx"); |
528 EXPECT_FALSE(InstallExtension(crx_path, 0)); | 529 EXPECT_FALSE(InstallExtension(crx_path, 0)); |
529 } | 530 } |
530 #endif | 531 #endif |
531 | 532 |
532 IN_PROC_BROWSER_TEST_F(ExtensionCrxInstallerTest, NonStrictManifestCheck) { | 533 IN_PROC_BROWSER_TEST_F(ExtensionCrxInstallerTest, NonStrictManifestCheck) { |
533 scoped_ptr<MockPromptProxy> mock_prompt = | 534 std::unique_ptr<MockPromptProxy> mock_prompt = |
534 CreateMockPromptProxyForBrowser(browser()); | 535 CreateMockPromptProxyForBrowser(browser()); |
535 | 536 |
536 // We want to simulate the case where the webstore sends a more recent | 537 // We want to simulate the case where the webstore sends a more recent |
537 // version of the manifest, but the downloaded .crx file is old since | 538 // version of the manifest, but the downloaded .crx file is old since |
538 // the newly published version hasn't fully propagated to all the download | 539 // the newly published version hasn't fully propagated to all the download |
539 // servers yet. So load the v2 manifest, but then install the v1 crx file. | 540 // servers yet. So load the v2 manifest, but then install the v1 crx file. |
540 std::string id = "lhnaeclnpobnlbjbgogdanmhadigfnjp"; | 541 std::string id = "lhnaeclnpobnlbjbgogdanmhadigfnjp"; |
541 scoped_ptr<WebstoreInstaller::Approval> approval = | 542 std::unique_ptr<WebstoreInstaller::Approval> approval = |
542 GetApproval("crx_installer/v2_no_permission_change/", id, false); | 543 GetApproval("crx_installer/v2_no_permission_change/", id, false); |
543 | 544 |
544 RunCrxInstaller(approval.get(), mock_prompt->CreatePrompt(), | 545 RunCrxInstaller(approval.get(), mock_prompt->CreatePrompt(), |
545 test_data_dir_.AppendASCII("crx_installer/v1.crx")); | 546 test_data_dir_.AppendASCII("crx_installer/v1.crx")); |
546 | 547 |
547 EXPECT_TRUE(mock_prompt->did_succeed()); | 548 EXPECT_TRUE(mock_prompt->did_succeed()); |
548 } | 549 } |
549 | 550 |
550 #if defined(OS_CHROMEOS) | 551 #if defined(OS_CHROMEOS) |
551 IN_PROC_BROWSER_TEST_F(ExtensionCrxInstallerTest, KioskOnlyTest) { | 552 IN_PROC_BROWSER_TEST_F(ExtensionCrxInstallerTest, KioskOnlyTest) { |
(...skipping 137 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
689 EXPECT_TRUE(WaitForCrxInstallerDone()); | 690 EXPECT_TRUE(WaitForCrxInstallerDone()); |
690 const Extension* extension = installer->extension(); | 691 const Extension* extension = installer->extension(); |
691 ASSERT_TRUE(extension); | 692 ASSERT_TRUE(extension); |
692 ASSERT_EQ(extension_id, extension->id()); | 693 ASSERT_EQ(extension_id, extension->id()); |
693 EXPECT_TRUE(ExtensionPrefs::Get(profile())->AllowFileAccess(extension_id)); | 694 EXPECT_TRUE(ExtensionPrefs::Get(profile())->AllowFileAccess(extension_id)); |
694 EXPECT_TRUE(extension->creation_flags() & Extension::ALLOW_FILE_ACCESS); | 695 EXPECT_TRUE(extension->creation_flags() & Extension::ALLOW_FILE_ACCESS); |
695 } | 696 } |
696 } | 697 } |
697 | 698 |
698 } // namespace extensions | 699 } // namespace extensions |
OLD | NEW |