| 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/common/extensions/api/extension_api.h" | 5 #include "chrome/common/extensions/api/extension_api.h" |
| 6 | 6 |
| 7 #include <string> | 7 #include <string> |
| 8 #include <vector> | 8 #include <vector> |
| 9 | 9 |
| 10 #include "base/file_util.h" | 10 #include "base/file_util.h" |
| 11 #include "base/files/file_path.h" | 11 #include "base/files/file_path.h" |
| 12 #include "base/json/json_reader.h" |
| 12 #include "base/json/json_writer.h" | 13 #include "base/json/json_writer.h" |
| 13 #include "base/memory/ref_counted.h" | 14 #include "base/memory/ref_counted.h" |
| 14 #include "base/memory/scoped_ptr.h" | 15 #include "base/memory/scoped_ptr.h" |
| 15 #include "base/path_service.h" | 16 #include "base/path_service.h" |
| 17 #include "base/stringprintf.h" |
| 16 #include "base/values.h" | 18 #include "base/values.h" |
| 17 #include "chrome/common/chrome_paths.h" | 19 #include "chrome/common/chrome_paths.h" |
| 18 #include "chrome/common/extensions/extension.h" | 20 #include "chrome/common/extensions/extension.h" |
| 21 #include "chrome/common/extensions/features/api_feature.h" |
| 22 #include "chrome/common/extensions/features/base_feature_provider.h" |
| 19 #include "chrome/common/extensions/features/simple_feature.h" | 23 #include "chrome/common/extensions/features/simple_feature.h" |
| 20 #include "chrome/common/extensions/manifest.h" | 24 #include "chrome/common/extensions/manifest.h" |
| 21 #include "testing/gtest/include/gtest/gtest.h" | 25 #include "testing/gtest/include/gtest/gtest.h" |
| 22 | 26 |
| 23 namespace extensions { | 27 namespace extensions { |
| 24 namespace { | 28 namespace { |
| 25 | 29 |
| 26 class TestFeatureProvider : public FeatureProvider { | 30 SimpleFeature* CreateAPIFeature() { |
| 27 public: | 31 return new APIFeature(); |
| 28 explicit TestFeatureProvider(Feature::Context context) | 32 } |
| 29 : context_(context) { | |
| 30 } | |
| 31 | |
| 32 virtual Feature* GetFeature(const std::string& name) OVERRIDE { | |
| 33 SimpleFeature* result = new SimpleFeature(); | |
| 34 result->set_name(name); | |
| 35 result->extension_types()->insert(Manifest::TYPE_EXTENSION); | |
| 36 result->GetContexts()->insert(context_); | |
| 37 to_destroy_.push_back(make_linked_ptr(result)); | |
| 38 return result; | |
| 39 } | |
| 40 | |
| 41 private: | |
| 42 std::vector<linked_ptr<Feature> > to_destroy_; | |
| 43 Feature::Context context_; | |
| 44 }; | |
| 45 | 33 |
| 46 TEST(ExtensionAPI, Creation) { | 34 TEST(ExtensionAPI, Creation) { |
| 47 ExtensionAPI* shared_instance = ExtensionAPI::GetSharedInstance(); | 35 ExtensionAPI* shared_instance = ExtensionAPI::GetSharedInstance(); |
| 48 EXPECT_EQ(shared_instance, ExtensionAPI::GetSharedInstance()); | 36 EXPECT_EQ(shared_instance, ExtensionAPI::GetSharedInstance()); |
| 49 | 37 |
| 50 scoped_ptr<ExtensionAPI> new_instance( | 38 scoped_ptr<ExtensionAPI> new_instance( |
| 51 ExtensionAPI::CreateWithDefaultConfiguration()); | 39 ExtensionAPI::CreateWithDefaultConfiguration()); |
| 52 EXPECT_NE(new_instance.get(), | 40 EXPECT_NE(new_instance.get(), |
| 53 scoped_ptr<ExtensionAPI>( | 41 scoped_ptr<ExtensionAPI>( |
| 54 ExtensionAPI::CreateWithDefaultConfiguration()).get()); | 42 ExtensionAPI::CreateWithDefaultConfiguration()).get()); |
| (...skipping 63 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 118 EXPECT_FALSE(extension_api->IsPrivileged("app.isInstalled")); | 106 EXPECT_FALSE(extension_api->IsPrivileged("app.isInstalled")); |
| 119 EXPECT_FALSE(extension_api->IsPrivileged("storage.local")); | 107 EXPECT_FALSE(extension_api->IsPrivileged("storage.local")); |
| 120 EXPECT_FALSE(extension_api->IsPrivileged("storage.local.onChanged")); | 108 EXPECT_FALSE(extension_api->IsPrivileged("storage.local.onChanged")); |
| 121 EXPECT_FALSE(extension_api->IsPrivileged("storage.local.set")); | 109 EXPECT_FALSE(extension_api->IsPrivileged("storage.local.set")); |
| 122 EXPECT_FALSE(extension_api->IsPrivileged("storage.local.MAX_ITEMS")); | 110 EXPECT_FALSE(extension_api->IsPrivileged("storage.local.MAX_ITEMS")); |
| 123 EXPECT_FALSE(extension_api->IsPrivileged("storage.set")); | 111 EXPECT_FALSE(extension_api->IsPrivileged("storage.set")); |
| 124 } | 112 } |
| 125 | 113 |
| 126 TEST(ExtensionAPI, IsPrivilegedFeatures) { | 114 TEST(ExtensionAPI, IsPrivilegedFeatures) { |
| 127 struct { | 115 struct { |
| 128 std::string filename; | |
| 129 std::string api_full_name; | 116 std::string api_full_name; |
| 130 bool expect_is_privilged; | 117 bool expect_is_privilged; |
| 131 Feature::Context test2_contexts; | |
| 132 } test_data[] = { | 118 } test_data[] = { |
| 133 { "is_privileged_features_1.json", "test", false, | 119 { "test1", false }, |
| 134 Feature::UNSPECIFIED_CONTEXT }, | 120 { "test1.foo", true }, |
| 135 { "is_privileged_features_2.json", "test", true, | 121 { "test2", true }, |
| 136 Feature::UNSPECIFIED_CONTEXT }, | 122 { "test2.foo", false }, |
| 137 { "is_privileged_features_3.json", "test", false, | 123 { "test2.bar", false }, |
| 138 Feature::UNSPECIFIED_CONTEXT }, | 124 { "test2.baz", true }, |
| 139 { "is_privileged_features_4.json", "test.bar", false, | 125 { "test3", false }, |
| 140 Feature::UNSPECIFIED_CONTEXT }, | 126 { "test3.foo", true }, |
| 141 { "is_privileged_features_5.json", "test.bar", true, | 127 { "test4", false } |
| 142 Feature::BLESSED_EXTENSION_CONTEXT }, | |
| 143 { "is_privileged_features_5.json", "test.bar", false, | |
| 144 Feature::UNBLESSED_EXTENSION_CONTEXT } | |
| 145 }; | 128 }; |
| 146 | 129 |
| 130 base::FilePath api_features_path; |
| 131 PathService::Get(chrome::DIR_TEST_DATA, &api_features_path); |
| 132 api_features_path = api_features_path.AppendASCII("extensions") |
| 133 .AppendASCII("extension_api_unittest") |
| 134 .AppendASCII("privileged_api_features.json"); |
| 135 |
| 136 std::string api_features_str; |
| 137 ASSERT_TRUE(file_util::ReadFileToString( |
| 138 api_features_path, &api_features_str)) << "privileged_api_features.json"; |
| 139 |
| 140 scoped_ptr<base::DictionaryValue> value(static_cast<DictionaryValue*>( |
| 141 base::JSONReader::Read(api_features_str))); |
| 142 BaseFeatureProvider api_feature_provider(*value, CreateAPIFeature); |
| 143 |
| 147 for (size_t i = 0; i < ARRAYSIZE_UNSAFE(test_data); ++i) { | 144 for (size_t i = 0; i < ARRAYSIZE_UNSAFE(test_data); ++i) { |
| 148 base::FilePath manifest_path; | |
| 149 PathService::Get(chrome::DIR_TEST_DATA, &manifest_path); | |
| 150 manifest_path = manifest_path.AppendASCII("extensions") | |
| 151 .AppendASCII("extension_api_unittest") | |
| 152 .AppendASCII(test_data[i].filename); | |
| 153 | |
| 154 std::string manifest_str; | |
| 155 ASSERT_TRUE(file_util::ReadFileToString(manifest_path, &manifest_str)) | |
| 156 << test_data[i].filename; | |
| 157 | |
| 158 ExtensionAPI api; | 145 ExtensionAPI api; |
| 159 api.RegisterSchema("test", manifest_str); | 146 api.RegisterDependencyProvider("api", &api_feature_provider); |
| 160 | |
| 161 TestFeatureProvider test2_provider(test_data[i].test2_contexts); | |
| 162 if (test_data[i].test2_contexts != Feature::UNSPECIFIED_CONTEXT) { | |
| 163 api.RegisterDependencyProvider("test2", &test2_provider); | |
| 164 } | |
| 165 | |
| 166 EXPECT_EQ(test_data[i].expect_is_privilged, | 147 EXPECT_EQ(test_data[i].expect_is_privilged, |
| 167 api.IsPrivileged(test_data[i].api_full_name)) << i; | 148 api.IsPrivileged(test_data[i].api_full_name)) << i; |
| 168 } | 149 } |
| 169 } | 150 } |
| 170 | 151 |
| 152 TEST(ExtensionAPI, APIFeatures) { |
| 153 struct { |
| 154 std::string api_full_name; |
| 155 bool expect_is_available; |
| 156 Feature::Context context; |
| 157 GURL url; |
| 158 } test_data[] = { |
| 159 { "test1", false, Feature::WEB_PAGE_CONTEXT, GURL() }, |
| 160 { "test1", true, Feature::BLESSED_EXTENSION_CONTEXT, GURL() }, |
| 161 { "test1", true, Feature::UNBLESSED_EXTENSION_CONTEXT, GURL() }, |
| 162 { "test1", true, Feature::CONTENT_SCRIPT_CONTEXT, GURL() }, |
| 163 { "test2", true, Feature::WEB_PAGE_CONTEXT, GURL("http://google.com") }, |
| 164 { "test2", false, Feature::BLESSED_EXTENSION_CONTEXT, |
| 165 GURL("http://google.com") }, |
| 166 { "test2.foo", false, Feature::WEB_PAGE_CONTEXT, |
| 167 GURL("http://google.com") }, |
| 168 { "test2.foo", true, Feature::CONTENT_SCRIPT_CONTEXT, GURL() }, |
| 169 { "test3", false, Feature::WEB_PAGE_CONTEXT, GURL("http://google.com") }, |
| 170 { "test3.foo", true, Feature::WEB_PAGE_CONTEXT, GURL("http://google.com") }, |
| 171 { "test3.foo", true, Feature::BLESSED_EXTENSION_CONTEXT, GURL() }, |
| 172 { "test4", true, Feature::BLESSED_EXTENSION_CONTEXT, GURL() }, |
| 173 { "test4.foo", false, Feature::BLESSED_EXTENSION_CONTEXT, GURL() }, |
| 174 { "test4.foo", false, Feature::UNBLESSED_EXTENSION_CONTEXT, GURL() }, |
| 175 { "test4.foo.foo", true, Feature::CONTENT_SCRIPT_CONTEXT, GURL() }, |
| 176 { "test5", true, Feature::WEB_PAGE_CONTEXT, GURL("http://foo.com") }, |
| 177 { "test5", false, Feature::WEB_PAGE_CONTEXT, GURL("http://bar.com") }, |
| 178 { "test5.blah", true, Feature::WEB_PAGE_CONTEXT, GURL("http://foo.com") }, |
| 179 { "test5.blah", false, Feature::WEB_PAGE_CONTEXT, GURL("http://bar.com") }, |
| 180 { "test6", false, Feature::BLESSED_EXTENSION_CONTEXT, GURL() }, |
| 181 { "test6.foo", true, Feature::BLESSED_EXTENSION_CONTEXT, GURL() }, |
| 182 { "test7", true, Feature::WEB_PAGE_CONTEXT, GURL("http://foo.com") }, |
| 183 { "test7.foo", false, Feature::WEB_PAGE_CONTEXT, GURL("http://bar.com") }, |
| 184 { "test7.foo", true, Feature::WEB_PAGE_CONTEXT, GURL("http://foo.com") }, |
| 185 { "test7.bar", false, Feature::WEB_PAGE_CONTEXT, GURL("http://bar.com") }, |
| 186 { "test7.bar", false, Feature::WEB_PAGE_CONTEXT, GURL("http://foo.com") } |
| 187 }; |
| 188 |
| 189 base::FilePath api_features_path; |
| 190 PathService::Get(chrome::DIR_TEST_DATA, &api_features_path); |
| 191 api_features_path = api_features_path.AppendASCII("extensions") |
| 192 .AppendASCII("extension_api_unittest") |
| 193 .AppendASCII("api_features.json"); |
| 194 |
| 195 std::string api_features_str; |
| 196 ASSERT_TRUE(file_util::ReadFileToString( |
| 197 api_features_path, &api_features_str)) << "api_features.json"; |
| 198 |
| 199 scoped_ptr<base::DictionaryValue> value(static_cast<DictionaryValue*>( |
| 200 base::JSONReader::Read(api_features_str))); |
| 201 BaseFeatureProvider api_feature_provider(*value, CreateAPIFeature); |
| 202 |
| 203 for (size_t i = 0; i < ARRAYSIZE_UNSAFE(test_data); ++i) { |
| 204 ExtensionAPI api; |
| 205 api.RegisterDependencyProvider("api", &api_feature_provider); |
| 206 for (base::DictionaryValue::Iterator iter(*value); !iter.IsAtEnd(); |
| 207 iter.Advance()) { |
| 208 if (iter.key().find(".") == std::string::npos) |
| 209 api.RegisterSchema(iter.key(), ""); |
| 210 } |
| 211 |
| 212 EXPECT_EQ(test_data[i].expect_is_available, |
| 213 api.IsAvailable(test_data[i].api_full_name, |
| 214 NULL, |
| 215 test_data[i].context, |
| 216 test_data[i].url).is_available()) << i; |
| 217 } |
| 218 } |
| 219 |
| 171 TEST(ExtensionAPI, LazyGetSchema) { | 220 TEST(ExtensionAPI, LazyGetSchema) { |
| 172 scoped_ptr<ExtensionAPI> apis(ExtensionAPI::CreateWithDefaultConfiguration()); | 221 scoped_ptr<ExtensionAPI> apis(ExtensionAPI::CreateWithDefaultConfiguration()); |
| 173 | 222 |
| 174 EXPECT_EQ(NULL, apis->GetSchema("")); | 223 EXPECT_EQ(NULL, apis->GetSchema("")); |
| 175 EXPECT_EQ(NULL, apis->GetSchema("")); | 224 EXPECT_EQ(NULL, apis->GetSchema("")); |
| 176 EXPECT_EQ(NULL, apis->GetSchema("experimental")); | 225 EXPECT_EQ(NULL, apis->GetSchema("experimental")); |
| 177 EXPECT_EQ(NULL, apis->GetSchema("experimental")); | 226 EXPECT_EQ(NULL, apis->GetSchema("experimental")); |
| 178 EXPECT_EQ(NULL, apis->GetSchema("foo")); | 227 EXPECT_EQ(NULL, apis->GetSchema("foo")); |
| 179 EXPECT_EQ(NULL, apis->GetSchema("foo")); | 228 EXPECT_EQ(NULL, apis->GetSchema("foo")); |
| 180 | 229 |
| (...skipping 188 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 369 std::string api_name = api->GetAPINameFromFullName(test_data[i].input, | 418 std::string api_name = api->GetAPINameFromFullName(test_data[i].input, |
| 370 &child_name); | 419 &child_name); |
| 371 EXPECT_EQ(test_data[i].api_name, api_name) << test_data[i].input; | 420 EXPECT_EQ(test_data[i].api_name, api_name) << test_data[i].input; |
| 372 EXPECT_EQ(test_data[i].child_name, child_name) << test_data[i].input; | 421 EXPECT_EQ(test_data[i].child_name, child_name) << test_data[i].input; |
| 373 } | 422 } |
| 374 } | 423 } |
| 375 | 424 |
| 376 TEST(ExtensionAPI, DefaultConfigurationFeatures) { | 425 TEST(ExtensionAPI, DefaultConfigurationFeatures) { |
| 377 scoped_ptr<ExtensionAPI> api(ExtensionAPI::CreateWithDefaultConfiguration()); | 426 scoped_ptr<ExtensionAPI> api(ExtensionAPI::CreateWithDefaultConfiguration()); |
| 378 | 427 |
| 379 SimpleFeature* bookmarks = | 428 SimpleFeature* bookmarks = static_cast<SimpleFeature*>( |
| 380 static_cast<SimpleFeature*>(api->GetFeature("bookmarks")); | 429 api->GetFeatureDependency("api:bookmarks")); |
| 381 SimpleFeature* bookmarks_create = | 430 SimpleFeature* bookmarks_create = static_cast<SimpleFeature*>( |
| 382 static_cast<SimpleFeature*>(api->GetFeature("bookmarks.create")); | 431 api->GetFeatureDependency("api:bookmarks.create")); |
| 383 | 432 |
| 384 struct { | 433 struct { |
| 385 SimpleFeature* feature; | 434 SimpleFeature* feature; |
| 386 // TODO(aa): More stuff to test over time. | 435 // TODO(aa): More stuff to test over time. |
| 387 } test_data[] = { | 436 } test_data[] = { |
| 388 { bookmarks }, | 437 { bookmarks }, |
| 389 { bookmarks_create } | 438 { bookmarks_create } |
| 390 }; | 439 }; |
| 391 | 440 |
| 392 for (size_t i = 0; i < ARRAYSIZE_UNSAFE(test_data); ++i) { | 441 for (size_t i = 0; i < ARRAYSIZE_UNSAFE(test_data); ++i) { |
| 393 SimpleFeature* feature = test_data[i].feature; | 442 SimpleFeature* feature = test_data[i].feature; |
| 394 ASSERT_TRUE(feature) << i; | 443 ASSERT_TRUE(feature) << i; |
| 395 | 444 |
| 396 EXPECT_TRUE(feature->whitelist()->empty()); | 445 EXPECT_TRUE(feature->whitelist()->empty()); |
| 397 EXPECT_TRUE(feature->extension_types()->empty()); | 446 EXPECT_TRUE(feature->extension_types()->empty()); |
| 398 | 447 |
| 399 EXPECT_EQ(1u, feature->GetContexts()->size()); | 448 EXPECT_EQ(1u, feature->GetContexts()->size()); |
| 400 EXPECT_TRUE(feature->GetContexts()->count( | 449 EXPECT_TRUE(feature->GetContexts()->count( |
| 401 Feature::BLESSED_EXTENSION_CONTEXT)); | 450 Feature::BLESSED_EXTENSION_CONTEXT)); |
| 402 | 451 |
| 403 EXPECT_EQ(Feature::UNSPECIFIED_LOCATION, feature->location()); | 452 EXPECT_EQ(Feature::UNSPECIFIED_LOCATION, feature->location()); |
| 404 EXPECT_EQ(Feature::UNSPECIFIED_PLATFORM, feature->platform()); | 453 EXPECT_EQ(Feature::UNSPECIFIED_PLATFORM, feature->platform()); |
| 405 EXPECT_EQ(0, feature->min_manifest_version()); | 454 EXPECT_EQ(0, feature->min_manifest_version()); |
| 406 EXPECT_EQ(0, feature->max_manifest_version()); | 455 EXPECT_EQ(0, feature->max_manifest_version()); |
| 407 } | 456 } |
| 408 } | 457 } |
| 409 | 458 |
| 410 TEST(ExtensionAPI, FeaturesRequireContexts) { | 459 TEST(ExtensionAPI, FeaturesRequireContexts) { |
| 411 scoped_ptr<base::ListValue> schema1(new base::ListValue()); | 460 // TODO(cduvall): Make this check API featues. |
| 412 base::DictionaryValue* feature_definition = new base::DictionaryValue(); | 461 scoped_ptr<base::DictionaryValue> api_features1(new base::DictionaryValue()); |
| 413 schema1->Append(feature_definition); | 462 scoped_ptr<base::DictionaryValue> api_features2(new base::DictionaryValue()); |
| 414 feature_definition->SetString("namespace", "test"); | 463 base::DictionaryValue* test1 = new base::DictionaryValue(); |
| 415 feature_definition->SetBoolean("uses_feature_system", true); | 464 base::DictionaryValue* test2 = new base::DictionaryValue(); |
| 416 | |
| 417 scoped_ptr<base::ListValue> schema2(schema1->DeepCopy()); | |
| 418 | |
| 419 base::ListValue* contexts = new base::ListValue(); | 465 base::ListValue* contexts = new base::ListValue(); |
| 420 contexts->Append(new base::StringValue("content_script")); | 466 contexts->Append(new base::StringValue("content_script")); |
| 421 feature_definition->Set("contexts", contexts); | 467 test1->Set("contexts", contexts); |
| 468 api_features1->Set("test", test1); |
| 469 api_features2->Set("test", test2); |
| 422 | 470 |
| 423 struct { | 471 struct { |
| 424 base::ListValue* schema; | 472 base::DictionaryValue* api_features; |
| 425 bool expect_success; | 473 bool expect_success; |
| 426 } test_data[] = { | 474 } test_data[] = { |
| 427 { schema1.get(), true }, | 475 { api_features1.get(), true }, |
| 428 { schema2.get(), false } | 476 { api_features2.get(), false } |
| 429 }; | 477 }; |
| 430 | 478 |
| 479 |
| 431 for (size_t i = 0; i < ARRAYSIZE_UNSAFE(test_data); ++i) { | 480 for (size_t i = 0; i < ARRAYSIZE_UNSAFE(test_data); ++i) { |
| 432 std::string schema_source; | 481 BaseFeatureProvider api_feature_provider(*test_data[i].api_features, |
| 433 base::JSONWriter::Write(test_data[i].schema, &schema_source); | 482 CreateAPIFeature); |
| 434 | 483 Feature* feature = api_feature_provider.GetFeature("test"); |
| 435 ExtensionAPI api; | |
| 436 api.RegisterSchema("test", base::StringPiece(schema_source)); | |
| 437 | |
| 438 Feature* feature = api.GetFeature("test"); | |
| 439 EXPECT_EQ(test_data[i].expect_success, feature != NULL) << i; | 484 EXPECT_EQ(test_data[i].expect_success, feature != NULL) << i; |
| 440 } | 485 } |
| 441 } | 486 } |
| 442 | 487 |
| 443 static void GetDictionaryFromList(const base::DictionaryValue* schema, | 488 static void GetDictionaryFromList(const base::DictionaryValue* schema, |
| 444 const std::string& list_name, | 489 const std::string& list_name, |
| 445 const int list_index, | 490 const int list_index, |
| 446 const base::DictionaryValue** out) { | 491 const base::DictionaryValue** out) { |
| 447 const base::ListValue* list; | 492 const base::ListValue* list; |
| 448 EXPECT_TRUE(schema->GetList(list_name, &list)); | 493 EXPECT_TRUE(schema->GetList(list_name, &list)); |
| (...skipping 54 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 503 GetDictionaryFromList(dict, "parameters", 0, &sub_dict); | 548 GetDictionaryFromList(dict, "parameters", 0, &sub_dict); |
| 504 EXPECT_TRUE(sub_dict->GetString("$ref", &type)); | 549 EXPECT_TRUE(sub_dict->GetString("$ref", &type)); |
| 505 EXPECT_EQ("test.foo.TestType", type); | 550 EXPECT_EQ("test.foo.TestType", type); |
| 506 GetDictionaryFromList(dict, "parameters", 1, &sub_dict); | 551 GetDictionaryFromList(dict, "parameters", 1, &sub_dict); |
| 507 EXPECT_TRUE(sub_dict->GetString("$ref", &type)); | 552 EXPECT_TRUE(sub_dict->GetString("$ref", &type)); |
| 508 EXPECT_EQ("fully.qualified.Type", type); | 553 EXPECT_EQ("fully.qualified.Type", type); |
| 509 } | 554 } |
| 510 | 555 |
| 511 } // namespace | 556 } // namespace |
| 512 } // namespace extensions | 557 } // namespace extensions |
| OLD | NEW |