| OLD | NEW |
| 1 // Copyright (c) 2013 The Chromium Authors. All rights reserved. | 1 // Copyright (c) 2013 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/extension.h" | 5 #include "chrome/common/extensions/extension.h" |
| 6 | 6 |
| 7 #include "base/base64.h" | 7 #include "base/base64.h" |
| 8 #include "base/basictypes.h" | 8 #include "base/basictypes.h" |
| 9 #include "base/command_line.h" | 9 #include "base/command_line.h" |
| 10 #include "base/file_util.h" | 10 #include "base/file_util.h" |
| (...skipping 126 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 137 } | 137 } |
| 138 ~ExtensionConfig() { } | 138 ~ExtensionConfig() { } |
| 139 | 139 |
| 140 // A whitelist of extensions that can script anywhere. Do not add to this | 140 // A whitelist of extensions that can script anywhere. Do not add to this |
| 141 // list (except in tests) without consulting the Extensions team first. | 141 // list (except in tests) without consulting the Extensions team first. |
| 142 // Note: Component extensions have this right implicitly and do not need to be | 142 // Note: Component extensions have this right implicitly and do not need to be |
| 143 // added to this list. | 143 // added to this list. |
| 144 Extension::ScriptingWhitelist scripting_whitelist_; | 144 Extension::ScriptingWhitelist scripting_whitelist_; |
| 145 }; | 145 }; |
| 146 | 146 |
| 147 bool ReadLaunchDimension(const extensions::Manifest* manifest, | |
| 148 const char* key, | |
| 149 int* target, | |
| 150 bool is_valid_container, | |
| 151 string16* error) { | |
| 152 const Value* temp = NULL; | |
| 153 if (manifest->Get(key, &temp)) { | |
| 154 if (!is_valid_container) { | |
| 155 *error = ErrorUtils::FormatErrorMessageUTF16( | |
| 156 errors::kInvalidLaunchValueContainer, | |
| 157 key); | |
| 158 return false; | |
| 159 } | |
| 160 if (!temp->GetAsInteger(target) || *target < 0) { | |
| 161 *target = 0; | |
| 162 *error = ErrorUtils::FormatErrorMessageUTF16( | |
| 163 errors::kInvalidLaunchValue, | |
| 164 key); | |
| 165 return false; | |
| 166 } | |
| 167 } | |
| 168 return true; | |
| 169 } | |
| 170 | |
| 171 bool ContainsManifestForbiddenPermission(const APIPermissionSet& apis, | 147 bool ContainsManifestForbiddenPermission(const APIPermissionSet& apis, |
| 172 string16* error) { | 148 string16* error) { |
| 173 CHECK(error); | 149 CHECK(error); |
| 174 for (APIPermissionSet::const_iterator i = apis.begin(); | 150 for (APIPermissionSet::const_iterator i = apis.begin(); |
| 175 i != apis.end(); ++i) { | 151 i != apis.end(); ++i) { |
| 176 if ((*i)->ManifestEntryForbidden()) { | 152 if ((*i)->ManifestEntryForbidden()) { |
| 177 *error = ErrorUtils::FormatErrorMessageUTF16( | 153 *error = ErrorUtils::FormatErrorMessageUTF16( |
| 178 errors::kPermissionNotAllowedInManifest, | 154 errors::kPermissionNotAllowedInManifest, |
| 179 (*i)->info()->name()); | 155 (*i)->info()->name()); |
| 180 return true; | 156 return true; |
| (...skipping 586 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 767 iter != browser_action->default_icon.map().end(); | 743 iter != browser_action->default_icon.map().end(); |
| 768 ++iter) { | 744 ++iter) { |
| 769 image_paths.insert( | 745 image_paths.insert( |
| 770 base::FilePath::FromWStringHack(UTF8ToWide(iter->second))); | 746 base::FilePath::FromWStringHack(UTF8ToWide(iter->second))); |
| 771 } | 747 } |
| 772 } | 748 } |
| 773 | 749 |
| 774 return image_paths; | 750 return image_paths; |
| 775 } | 751 } |
| 776 | 752 |
| 777 GURL Extension::GetFullLaunchURL() const { | |
| 778 return launch_local_path().empty() ? GURL(launch_web_url()) : | |
| 779 url().Resolve(launch_local_path()); | |
| 780 } | |
| 781 | |
| 782 bool Extension::CanExecuteScriptOnPage(const GURL& document_url, | 753 bool Extension::CanExecuteScriptOnPage(const GURL& document_url, |
| 783 const GURL& top_frame_url, | 754 const GURL& top_frame_url, |
| 784 int tab_id, | 755 int tab_id, |
| 785 const UserScript* script, | 756 const UserScript* script, |
| 786 std::string* error) const { | 757 std::string* error) const { |
| 787 base::AutoLock auto_lock(runtime_data_lock_); | 758 base::AutoLock auto_lock(runtime_data_lock_); |
| 788 // The gallery is special-cased as a restricted URL for scripting to prevent | 759 // The gallery is special-cased as a restricted URL for scripting to prevent |
| 789 // access to special JS bindings we expose to the gallery (and avoid things | 760 // access to special JS bindings we expose to the gallery (and avoid things |
| 790 // like extensions removing the "report abuse" link). | 761 // like extensions removing the "report abuse" link). |
| 791 // TODO(erikkay): This seems like the wrong test. Shouldn't we we testing | 762 // TODO(erikkay): This seems like the wrong test. Shouldn't we we testing |
| (...skipping 290 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 1082 } | 1053 } |
| 1083 | 1054 |
| 1084 bool Extension::can_be_incognito_enabled() const { | 1055 bool Extension::can_be_incognito_enabled() const { |
| 1085 return !is_platform_app(); | 1056 return !is_platform_app(); |
| 1086 } | 1057 } |
| 1087 | 1058 |
| 1088 void Extension::AddWebExtentPattern(const URLPattern& pattern) { | 1059 void Extension::AddWebExtentPattern(const URLPattern& pattern) { |
| 1089 extent_.AddPattern(pattern); | 1060 extent_.AddPattern(pattern); |
| 1090 } | 1061 } |
| 1091 | 1062 |
| 1063 void Extension::ClearWebExtentPatterns() { |
| 1064 extent_.ClearPatterns(); |
| 1065 } |
| 1066 |
| 1092 bool Extension::is_theme() const { | 1067 bool Extension::is_theme() const { |
| 1093 return manifest()->is_theme(); | 1068 return manifest()->is_theme(); |
| 1094 } | 1069 } |
| 1095 | 1070 |
| 1096 bool Extension::is_content_pack() const { | 1071 bool Extension::is_content_pack() const { |
| 1097 return !content_pack_site_list_.empty(); | 1072 return !content_pack_site_list_.empty(); |
| 1098 } | 1073 } |
| 1099 | 1074 |
| 1100 ExtensionResource Extension::GetContentPackSiteList() const { | 1075 ExtensionResource Extension::GetContentPackSiteList() const { |
| 1101 if (!is_content_pack()) | 1076 if (!is_content_pack()) |
| (...skipping 150 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 1252 | 1227 |
| 1253 Extension::Extension(const base::FilePath& path, | 1228 Extension::Extension(const base::FilePath& path, |
| 1254 scoped_ptr<extensions::Manifest> manifest) | 1229 scoped_ptr<extensions::Manifest> manifest) |
| 1255 : manifest_version_(0), | 1230 : manifest_version_(0), |
| 1256 incognito_split_mode_(false), | 1231 incognito_split_mode_(false), |
| 1257 offline_enabled_(false), | 1232 offline_enabled_(false), |
| 1258 converted_from_user_script_(false), | 1233 converted_from_user_script_(false), |
| 1259 manifest_(manifest.release()), | 1234 manifest_(manifest.release()), |
| 1260 finished_parsing_manifest_(false), | 1235 finished_parsing_manifest_(false), |
| 1261 is_storage_isolated_(false), | 1236 is_storage_isolated_(false), |
| 1262 launch_container_(extension_misc::LAUNCH_TAB), | |
| 1263 launch_width_(0), | |
| 1264 launch_height_(0), | |
| 1265 display_in_launcher_(true), | 1237 display_in_launcher_(true), |
| 1266 display_in_new_tab_page_(true), | 1238 display_in_new_tab_page_(true), |
| 1267 wants_file_access_(false), | 1239 wants_file_access_(false), |
| 1268 creation_flags_(0) { | 1240 creation_flags_(0) { |
| 1269 DCHECK(path.empty() || path.IsAbsolute()); | 1241 DCHECK(path.empty() || path.IsAbsolute()); |
| 1270 path_ = MaybeNormalizePath(path); | 1242 path_ = MaybeNormalizePath(path); |
| 1271 } | 1243 } |
| 1272 | 1244 |
| 1273 Extension::~Extension() { | 1245 Extension::~Extension() { |
| 1274 } | 1246 } |
| (...skipping 176 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 1451 version_.reset(new Version(version_str)); | 1423 version_.reset(new Version(version_str)); |
| 1452 if (!version_->IsValid() || version_->components().size() > 4) { | 1424 if (!version_->IsValid() || version_->components().size() > 4) { |
| 1453 *error = ASCIIToUTF16(errors::kInvalidVersion); | 1425 *error = ASCIIToUTF16(errors::kInvalidVersion); |
| 1454 return false; | 1426 return false; |
| 1455 } | 1427 } |
| 1456 return true; | 1428 return true; |
| 1457 } | 1429 } |
| 1458 | 1430 |
| 1459 bool Extension::LoadAppFeatures(string16* error) { | 1431 bool Extension::LoadAppFeatures(string16* error) { |
| 1460 if (!LoadExtent(keys::kWebURLs, &extent_, | 1432 if (!LoadExtent(keys::kWebURLs, &extent_, |
| 1461 errors::kInvalidWebURLs, errors::kInvalidWebURL, error) || | 1433 errors::kInvalidWebURLs, errors::kInvalidWebURL, error)) { |
| 1462 !LoadLaunchURL(error) || | |
| 1463 !LoadLaunchContainer(error)) { | |
| 1464 return false; | 1434 return false; |
| 1465 } | 1435 } |
| 1466 if (manifest_->HasKey(keys::kDisplayInLauncher) && | 1436 if (manifest_->HasKey(keys::kDisplayInLauncher) && |
| 1467 !manifest_->GetBoolean(keys::kDisplayInLauncher, &display_in_launcher_)) { | 1437 !manifest_->GetBoolean(keys::kDisplayInLauncher, &display_in_launcher_)) { |
| 1468 *error = ASCIIToUTF16(errors::kInvalidDisplayInLauncher); | 1438 *error = ASCIIToUTF16(errors::kInvalidDisplayInLauncher); |
| 1469 return false; | 1439 return false; |
| 1470 } | 1440 } |
| 1471 if (manifest_->HasKey(keys::kDisplayInNewTabPage)) { | 1441 if (manifest_->HasKey(keys::kDisplayInNewTabPage)) { |
| 1472 if (!manifest_->GetBoolean(keys::kDisplayInNewTabPage, | 1442 if (!manifest_->GetBoolean(keys::kDisplayInNewTabPage, |
| 1473 &display_in_new_tab_page_)) { | 1443 &display_in_new_tab_page_)) { |
| (...skipping 74 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 1548 return false; | 1518 return false; |
| 1549 } | 1519 } |
| 1550 pattern.SetPath(pattern.path() + '*'); | 1520 pattern.SetPath(pattern.path() + '*'); |
| 1551 | 1521 |
| 1552 extent->AddPattern(pattern); | 1522 extent->AddPattern(pattern); |
| 1553 } | 1523 } |
| 1554 | 1524 |
| 1555 return true; | 1525 return true; |
| 1556 } | 1526 } |
| 1557 | 1527 |
| 1558 bool Extension::LoadLaunchContainer(string16* error) { | |
| 1559 const Value* tmp_launcher_container = NULL; | |
| 1560 if (!manifest_->Get(keys::kLaunchContainer, &tmp_launcher_container)) | |
| 1561 return true; | |
| 1562 | |
| 1563 std::string launch_container_string; | |
| 1564 if (!tmp_launcher_container->GetAsString(&launch_container_string)) { | |
| 1565 *error = ASCIIToUTF16(errors::kInvalidLaunchContainer); | |
| 1566 return false; | |
| 1567 } | |
| 1568 | |
| 1569 if (launch_container_string == values::kLaunchContainerPanel) { | |
| 1570 launch_container_ = extension_misc::LAUNCH_PANEL; | |
| 1571 } else if (launch_container_string == values::kLaunchContainerTab) { | |
| 1572 launch_container_ = extension_misc::LAUNCH_TAB; | |
| 1573 } else { | |
| 1574 *error = ASCIIToUTF16(errors::kInvalidLaunchContainer); | |
| 1575 return false; | |
| 1576 } | |
| 1577 | |
| 1578 bool can_specify_initial_size = | |
| 1579 launch_container_ == extension_misc::LAUNCH_PANEL || | |
| 1580 launch_container_ == extension_misc::LAUNCH_WINDOW; | |
| 1581 | |
| 1582 // Validate the container width if present. | |
| 1583 if (!ReadLaunchDimension(manifest_.get(), | |
| 1584 keys::kLaunchWidth, | |
| 1585 &launch_width_, | |
| 1586 can_specify_initial_size, | |
| 1587 error)) { | |
| 1588 return false; | |
| 1589 } | |
| 1590 | |
| 1591 // Validate container height if present. | |
| 1592 if (!ReadLaunchDimension(manifest_.get(), | |
| 1593 keys::kLaunchHeight, | |
| 1594 &launch_height_, | |
| 1595 can_specify_initial_size, | |
| 1596 error)) { | |
| 1597 return false; | |
| 1598 } | |
| 1599 | |
| 1600 return true; | |
| 1601 } | |
| 1602 | |
| 1603 bool Extension::LoadLaunchURL(string16* error) { | |
| 1604 const Value* temp = NULL; | |
| 1605 | |
| 1606 // launch URL can be either local (to chrome-extension:// root) or an absolute | |
| 1607 // web URL. | |
| 1608 if (manifest_->Get(keys::kLaunchLocalPath, &temp)) { | |
| 1609 if (manifest_->Get(keys::kLaunchWebURL, NULL)) { | |
| 1610 *error = ASCIIToUTF16(errors::kLaunchPathAndURLAreExclusive); | |
| 1611 return false; | |
| 1612 } | |
| 1613 | |
| 1614 if (manifest_->Get(keys::kWebURLs, NULL)) { | |
| 1615 *error = ASCIIToUTF16(errors::kLaunchPathAndExtentAreExclusive); | |
| 1616 return false; | |
| 1617 } | |
| 1618 | |
| 1619 std::string launch_path; | |
| 1620 if (!temp->GetAsString(&launch_path)) { | |
| 1621 *error = ErrorUtils::FormatErrorMessageUTF16( | |
| 1622 errors::kInvalidLaunchValue, | |
| 1623 keys::kLaunchLocalPath); | |
| 1624 return false; | |
| 1625 } | |
| 1626 | |
| 1627 // Ensure the launch path is a valid relative URL. | |
| 1628 GURL resolved = url().Resolve(launch_path); | |
| 1629 if (!resolved.is_valid() || resolved.GetOrigin() != url()) { | |
| 1630 *error = ErrorUtils::FormatErrorMessageUTF16( | |
| 1631 errors::kInvalidLaunchValue, | |
| 1632 keys::kLaunchLocalPath); | |
| 1633 return false; | |
| 1634 } | |
| 1635 | |
| 1636 launch_local_path_ = launch_path; | |
| 1637 } else if (manifest_->Get(keys::kLaunchWebURL, &temp)) { | |
| 1638 std::string launch_url; | |
| 1639 if (!temp->GetAsString(&launch_url)) { | |
| 1640 *error = ErrorUtils::FormatErrorMessageUTF16( | |
| 1641 errors::kInvalidLaunchValue, | |
| 1642 keys::kLaunchWebURL); | |
| 1643 return false; | |
| 1644 } | |
| 1645 | |
| 1646 // Ensure the launch URL is a valid absolute URL and web extent scheme. | |
| 1647 GURL url(launch_url); | |
| 1648 URLPattern pattern(kValidWebExtentSchemes); | |
| 1649 if (!url.is_valid() || !pattern.SetScheme(url.scheme())) { | |
| 1650 *error = ErrorUtils::FormatErrorMessageUTF16( | |
| 1651 errors::kInvalidLaunchValue, | |
| 1652 keys::kLaunchWebURL); | |
| 1653 return false; | |
| 1654 } | |
| 1655 | |
| 1656 launch_web_url_ = launch_url; | |
| 1657 } else if (is_legacy_packaged_app() || is_hosted_app()) { | |
| 1658 *error = ASCIIToUTF16(errors::kLaunchURLRequired); | |
| 1659 return false; | |
| 1660 } | |
| 1661 | |
| 1662 // If there is no extent, we default the extent based on the launch URL. | |
| 1663 if (web_extent().is_empty() && !launch_web_url().empty()) { | |
| 1664 GURL launch_url(launch_web_url()); | |
| 1665 URLPattern pattern(kValidWebExtentSchemes); | |
| 1666 if (!pattern.SetScheme("*")) { | |
| 1667 *error = ErrorUtils::FormatErrorMessageUTF16( | |
| 1668 errors::kInvalidLaunchValue, | |
| 1669 keys::kLaunchWebURL); | |
| 1670 return false; | |
| 1671 } | |
| 1672 pattern.SetHost(launch_url.host()); | |
| 1673 pattern.SetPath("/*"); | |
| 1674 extent_.AddPattern(pattern); | |
| 1675 } | |
| 1676 | |
| 1677 // In order for the --apps-gallery-url switch to work with the gallery | |
| 1678 // process isolation, we must insert any provided value into the component | |
| 1679 // app's launch url and web extent. | |
| 1680 if (id() == extension_misc::kWebStoreAppId) { | |
| 1681 std::string gallery_url_str = CommandLine::ForCurrentProcess()-> | |
| 1682 GetSwitchValueASCII(switches::kAppsGalleryURL); | |
| 1683 | |
| 1684 // Empty string means option was not used. | |
| 1685 if (!gallery_url_str.empty()) { | |
| 1686 GURL gallery_url(gallery_url_str); | |
| 1687 OverrideLaunchUrl(gallery_url); | |
| 1688 } | |
| 1689 } else if (id() == extension_misc::kCloudPrintAppId) { | |
| 1690 // In order for the --cloud-print-service switch to work, we must update | |
| 1691 // the launch URL and web extent. | |
| 1692 // TODO(sanjeevr): Ideally we want to use CloudPrintURL here but that is | |
| 1693 // currently under chrome/browser. | |
| 1694 const CommandLine& command_line = *CommandLine::ForCurrentProcess(); | |
| 1695 GURL cloud_print_service_url = GURL(command_line.GetSwitchValueASCII( | |
| 1696 switches::kCloudPrintServiceURL)); | |
| 1697 if (!cloud_print_service_url.is_empty()) { | |
| 1698 std::string path( | |
| 1699 cloud_print_service_url.path() + "/enable_chrome_connector"); | |
| 1700 GURL::Replacements replacements; | |
| 1701 replacements.SetPathStr(path); | |
| 1702 GURL cloud_print_enable_connector_url = | |
| 1703 cloud_print_service_url.ReplaceComponents(replacements); | |
| 1704 OverrideLaunchUrl(cloud_print_enable_connector_url); | |
| 1705 } | |
| 1706 } else if (id() == extension_misc::kChromeAppId) { | |
| 1707 // Override launch url to new tab. | |
| 1708 launch_web_url_ = chrome::kChromeUINewTabURL; | |
| 1709 extent_.ClearPatterns(); | |
| 1710 } | |
| 1711 | |
| 1712 return true; | |
| 1713 } | |
| 1714 | |
| 1715 bool Extension::LoadSharedFeatures(string16* error) { | 1528 bool Extension::LoadSharedFeatures(string16* error) { |
| 1716 if (!LoadDescription(error) || | 1529 if (!LoadDescription(error) || |
| 1717 !ManifestHandler::ParseExtension(this, error) || | 1530 !ManifestHandler::ParseExtension(this, error) || |
| 1718 !LoadPlugins(error) || | 1531 !LoadPlugins(error) || |
| 1719 !LoadNaClModules(error) || | 1532 !LoadNaClModules(error) || |
| 1720 !LoadSandboxedPages(error) || | 1533 !LoadSandboxedPages(error) || |
| 1721 !LoadRequirements(error) || | 1534 !LoadRequirements(error) || |
| 1722 !LoadOfflineEnabled(error)) | 1535 !LoadOfflineEnabled(error)) |
| 1723 return false; | 1536 return false; |
| 1724 | 1537 |
| (...skipping 613 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 2338 | 2151 |
| 2339 if (ActionInfo::GetBrowserActionInfo(this)) | 2152 if (ActionInfo::GetBrowserActionInfo(this)) |
| 2340 ++num_surfaces; | 2153 ++num_surfaces; |
| 2341 | 2154 |
| 2342 if (is_app()) | 2155 if (is_app()) |
| 2343 ++num_surfaces; | 2156 ++num_surfaces; |
| 2344 | 2157 |
| 2345 return num_surfaces > 1; | 2158 return num_surfaces > 1; |
| 2346 } | 2159 } |
| 2347 | 2160 |
| 2348 void Extension::OverrideLaunchUrl(const GURL& override_url) { | |
| 2349 GURL new_url(override_url); | |
| 2350 if (!new_url.is_valid()) { | |
| 2351 DLOG(WARNING) << "Invalid override url given for " << name(); | |
| 2352 } else { | |
| 2353 if (new_url.has_port()) { | |
| 2354 DLOG(WARNING) << "Override URL passed for " << name() | |
| 2355 << " should not contain a port. Removing it."; | |
| 2356 | |
| 2357 GURL::Replacements remove_port; | |
| 2358 remove_port.ClearPort(); | |
| 2359 new_url = new_url.ReplaceComponents(remove_port); | |
| 2360 } | |
| 2361 | |
| 2362 launch_web_url_ = new_url.spec(); | |
| 2363 | |
| 2364 URLPattern pattern(kValidWebExtentSchemes); | |
| 2365 URLPattern::ParseResult result = pattern.Parse(new_url.spec()); | |
| 2366 DCHECK_EQ(result, URLPattern::PARSE_SUCCESS); | |
| 2367 pattern.SetPath(pattern.path() + '*'); | |
| 2368 extent_.AddPattern(pattern); | |
| 2369 } | |
| 2370 } | |
| 2371 | |
| 2372 bool Extension::CanSpecifyExperimentalPermission() const { | 2161 bool Extension::CanSpecifyExperimentalPermission() const { |
| 2373 if (location() == Manifest::COMPONENT) | 2162 if (location() == Manifest::COMPONENT) |
| 2374 return true; | 2163 return true; |
| 2375 | 2164 |
| 2376 if (CommandLine::ForCurrentProcess()->HasSwitch( | 2165 if (CommandLine::ForCurrentProcess()->HasSwitch( |
| 2377 switches::kEnableExperimentalExtensionApis)) { | 2166 switches::kEnableExperimentalExtensionApis)) { |
| 2378 return true; | 2167 return true; |
| 2379 } | 2168 } |
| 2380 | 2169 |
| 2381 // We rely on the webstore to check access to experimental. This way we can | 2170 // We rely on the webstore to check access to experimental. This way we can |
| (...skipping 117 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 2499 | 2288 |
| 2500 UpdatedExtensionPermissionsInfo::UpdatedExtensionPermissionsInfo( | 2289 UpdatedExtensionPermissionsInfo::UpdatedExtensionPermissionsInfo( |
| 2501 const Extension* extension, | 2290 const Extension* extension, |
| 2502 const PermissionSet* permissions, | 2291 const PermissionSet* permissions, |
| 2503 Reason reason) | 2292 Reason reason) |
| 2504 : reason(reason), | 2293 : reason(reason), |
| 2505 extension(extension), | 2294 extension(extension), |
| 2506 permissions(permissions) {} | 2295 permissions(permissions) {} |
| 2507 | 2296 |
| 2508 } // namespace extensions | 2297 } // namespace extensions |
| OLD | NEW |