| 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 612 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 793 iter != browser_action->default_icon.map().end(); | 769 iter != browser_action->default_icon.map().end(); |
| 794 ++iter) { | 770 ++iter) { |
| 795 image_paths.insert( | 771 image_paths.insert( |
| 796 base::FilePath::FromWStringHack(UTF8ToWide(iter->second))); | 772 base::FilePath::FromWStringHack(UTF8ToWide(iter->second))); |
| 797 } | 773 } |
| 798 } | 774 } |
| 799 | 775 |
| 800 return image_paths; | 776 return image_paths; |
| 801 } | 777 } |
| 802 | 778 |
| 803 GURL Extension::GetFullLaunchURL() const { | |
| 804 return launch_local_path().empty() ? GURL(launch_web_url()) : | |
| 805 url().Resolve(launch_local_path()); | |
| 806 } | |
| 807 | |
| 808 bool Extension::CanExecuteScriptOnPage(const GURL& document_url, | 779 bool Extension::CanExecuteScriptOnPage(const GURL& document_url, |
| 809 const GURL& top_frame_url, | 780 const GURL& top_frame_url, |
| 810 int tab_id, | 781 int tab_id, |
| 811 const UserScript* script, | 782 const UserScript* script, |
| 812 std::string* error) const { | 783 std::string* error) const { |
| 813 base::AutoLock auto_lock(runtime_data_lock_); | 784 base::AutoLock auto_lock(runtime_data_lock_); |
| 814 // The gallery is special-cased as a restricted URL for scripting to prevent | 785 // The gallery is special-cased as a restricted URL for scripting to prevent |
| 815 // access to special JS bindings we expose to the gallery (and avoid things | 786 // access to special JS bindings we expose to the gallery (and avoid things |
| 816 // like extensions removing the "report abuse" link). | 787 // like extensions removing the "report abuse" link). |
| 817 // TODO(erikkay): This seems like the wrong test. Shouldn't we we testing | 788 // TODO(erikkay): This seems like the wrong test. Shouldn't we we testing |
| (...skipping 296 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 1114 } | 1085 } |
| 1115 | 1086 |
| 1116 bool Extension::can_be_incognito_enabled() const { | 1087 bool Extension::can_be_incognito_enabled() const { |
| 1117 return !is_platform_app(); | 1088 return !is_platform_app(); |
| 1118 } | 1089 } |
| 1119 | 1090 |
| 1120 void Extension::AddWebExtentPattern(const URLPattern& pattern) { | 1091 void Extension::AddWebExtentPattern(const URLPattern& pattern) { |
| 1121 extent_.AddPattern(pattern); | 1092 extent_.AddPattern(pattern); |
| 1122 } | 1093 } |
| 1123 | 1094 |
| 1095 void Extension::ClearWebExtentPatterns() { |
| 1096 extent_.ClearPatterns(); |
| 1097 } |
| 1098 |
| 1124 bool Extension::is_theme() const { | 1099 bool Extension::is_theme() const { |
| 1125 return manifest()->is_theme(); | 1100 return manifest()->is_theme(); |
| 1126 } | 1101 } |
| 1127 | 1102 |
| 1128 bool Extension::is_content_pack() const { | 1103 bool Extension::is_content_pack() const { |
| 1129 return !content_pack_site_list_.empty(); | 1104 return !content_pack_site_list_.empty(); |
| 1130 } | 1105 } |
| 1131 | 1106 |
| 1132 ExtensionResource Extension::GetContentPackSiteList() const { | 1107 ExtensionResource Extension::GetContentPackSiteList() const { |
| 1133 if (!is_content_pack()) | 1108 if (!is_content_pack()) |
| (...skipping 151 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 1285 Extension::Extension(const base::FilePath& path, | 1260 Extension::Extension(const base::FilePath& path, |
| 1286 scoped_ptr<extensions::Manifest> manifest) | 1261 scoped_ptr<extensions::Manifest> manifest) |
| 1287 : manifest_version_(0), | 1262 : manifest_version_(0), |
| 1288 incognito_split_mode_(false), | 1263 incognito_split_mode_(false), |
| 1289 kiosk_enabled_(false), | 1264 kiosk_enabled_(false), |
| 1290 offline_enabled_(false), | 1265 offline_enabled_(false), |
| 1291 converted_from_user_script_(false), | 1266 converted_from_user_script_(false), |
| 1292 manifest_(manifest.release()), | 1267 manifest_(manifest.release()), |
| 1293 finished_parsing_manifest_(false), | 1268 finished_parsing_manifest_(false), |
| 1294 is_storage_isolated_(false), | 1269 is_storage_isolated_(false), |
| 1295 launch_container_(extension_misc::LAUNCH_TAB), | |
| 1296 launch_width_(0), | |
| 1297 launch_height_(0), | |
| 1298 display_in_launcher_(true), | 1270 display_in_launcher_(true), |
| 1299 display_in_new_tab_page_(true), | 1271 display_in_new_tab_page_(true), |
| 1300 wants_file_access_(false), | 1272 wants_file_access_(false), |
| 1301 creation_flags_(0) { | 1273 creation_flags_(0) { |
| 1302 DCHECK(path.empty() || path.IsAbsolute()); | 1274 DCHECK(path.empty() || path.IsAbsolute()); |
| 1303 path_ = MaybeNormalizePath(path); | 1275 path_ = MaybeNormalizePath(path); |
| 1304 } | 1276 } |
| 1305 | 1277 |
| 1306 Extension::~Extension() { | 1278 Extension::~Extension() { |
| 1307 } | 1279 } |
| (...skipping 176 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 1484 version_.reset(new Version(version_str)); | 1456 version_.reset(new Version(version_str)); |
| 1485 if (!version_->IsValid() || version_->components().size() > 4) { | 1457 if (!version_->IsValid() || version_->components().size() > 4) { |
| 1486 *error = ASCIIToUTF16(errors::kInvalidVersion); | 1458 *error = ASCIIToUTF16(errors::kInvalidVersion); |
| 1487 return false; | 1459 return false; |
| 1488 } | 1460 } |
| 1489 return true; | 1461 return true; |
| 1490 } | 1462 } |
| 1491 | 1463 |
| 1492 bool Extension::LoadAppFeatures(string16* error) { | 1464 bool Extension::LoadAppFeatures(string16* error) { |
| 1493 if (!LoadExtent(keys::kWebURLs, &extent_, | 1465 if (!LoadExtent(keys::kWebURLs, &extent_, |
| 1494 errors::kInvalidWebURLs, errors::kInvalidWebURL, error) || | 1466 errors::kInvalidWebURLs, errors::kInvalidWebURL, error)) { |
| 1495 !LoadLaunchURL(error) || | |
| 1496 !LoadLaunchContainer(error)) { | |
| 1497 return false; | 1467 return false; |
| 1498 } | 1468 } |
| 1499 if (manifest_->HasKey(keys::kDisplayInLauncher) && | 1469 if (manifest_->HasKey(keys::kDisplayInLauncher) && |
| 1500 !manifest_->GetBoolean(keys::kDisplayInLauncher, &display_in_launcher_)) { | 1470 !manifest_->GetBoolean(keys::kDisplayInLauncher, &display_in_launcher_)) { |
| 1501 *error = ASCIIToUTF16(errors::kInvalidDisplayInLauncher); | 1471 *error = ASCIIToUTF16(errors::kInvalidDisplayInLauncher); |
| 1502 return false; | 1472 return false; |
| 1503 } | 1473 } |
| 1504 if (manifest_->HasKey(keys::kDisplayInNewTabPage)) { | 1474 if (manifest_->HasKey(keys::kDisplayInNewTabPage)) { |
| 1505 if (!manifest_->GetBoolean(keys::kDisplayInNewTabPage, | 1475 if (!manifest_->GetBoolean(keys::kDisplayInNewTabPage, |
| 1506 &display_in_new_tab_page_)) { | 1476 &display_in_new_tab_page_)) { |
| (...skipping 74 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 1581 return false; | 1551 return false; |
| 1582 } | 1552 } |
| 1583 pattern.SetPath(pattern.path() + '*'); | 1553 pattern.SetPath(pattern.path() + '*'); |
| 1584 | 1554 |
| 1585 extent->AddPattern(pattern); | 1555 extent->AddPattern(pattern); |
| 1586 } | 1556 } |
| 1587 | 1557 |
| 1588 return true; | 1558 return true; |
| 1589 } | 1559 } |
| 1590 | 1560 |
| 1591 bool Extension::LoadLaunchContainer(string16* error) { | |
| 1592 const Value* tmp_launcher_container = NULL; | |
| 1593 if (!manifest_->Get(keys::kLaunchContainer, &tmp_launcher_container)) | |
| 1594 return true; | |
| 1595 | |
| 1596 std::string launch_container_string; | |
| 1597 if (!tmp_launcher_container->GetAsString(&launch_container_string)) { | |
| 1598 *error = ASCIIToUTF16(errors::kInvalidLaunchContainer); | |
| 1599 return false; | |
| 1600 } | |
| 1601 | |
| 1602 if (launch_container_string == values::kLaunchContainerPanel) { | |
| 1603 launch_container_ = extension_misc::LAUNCH_PANEL; | |
| 1604 } else if (launch_container_string == values::kLaunchContainerTab) { | |
| 1605 launch_container_ = extension_misc::LAUNCH_TAB; | |
| 1606 } else { | |
| 1607 *error = ASCIIToUTF16(errors::kInvalidLaunchContainer); | |
| 1608 return false; | |
| 1609 } | |
| 1610 | |
| 1611 bool can_specify_initial_size = | |
| 1612 launch_container_ == extension_misc::LAUNCH_PANEL || | |
| 1613 launch_container_ == extension_misc::LAUNCH_WINDOW; | |
| 1614 | |
| 1615 // Validate the container width if present. | |
| 1616 if (!ReadLaunchDimension(manifest_.get(), | |
| 1617 keys::kLaunchWidth, | |
| 1618 &launch_width_, | |
| 1619 can_specify_initial_size, | |
| 1620 error)) { | |
| 1621 return false; | |
| 1622 } | |
| 1623 | |
| 1624 // Validate container height if present. | |
| 1625 if (!ReadLaunchDimension(manifest_.get(), | |
| 1626 keys::kLaunchHeight, | |
| 1627 &launch_height_, | |
| 1628 can_specify_initial_size, | |
| 1629 error)) { | |
| 1630 return false; | |
| 1631 } | |
| 1632 | |
| 1633 return true; | |
| 1634 } | |
| 1635 | |
| 1636 bool Extension::LoadLaunchURL(string16* error) { | |
| 1637 const Value* temp = NULL; | |
| 1638 | |
| 1639 // launch URL can be either local (to chrome-extension:// root) or an absolute | |
| 1640 // web URL. | |
| 1641 if (manifest_->Get(keys::kLaunchLocalPath, &temp)) { | |
| 1642 if (manifest_->Get(keys::kLaunchWebURL, NULL)) { | |
| 1643 *error = ASCIIToUTF16(errors::kLaunchPathAndURLAreExclusive); | |
| 1644 return false; | |
| 1645 } | |
| 1646 | |
| 1647 if (manifest_->Get(keys::kWebURLs, NULL)) { | |
| 1648 *error = ASCIIToUTF16(errors::kLaunchPathAndExtentAreExclusive); | |
| 1649 return false; | |
| 1650 } | |
| 1651 | |
| 1652 std::string launch_path; | |
| 1653 if (!temp->GetAsString(&launch_path)) { | |
| 1654 *error = ErrorUtils::FormatErrorMessageUTF16( | |
| 1655 errors::kInvalidLaunchValue, | |
| 1656 keys::kLaunchLocalPath); | |
| 1657 return false; | |
| 1658 } | |
| 1659 | |
| 1660 // Ensure the launch path is a valid relative URL. | |
| 1661 GURL resolved = url().Resolve(launch_path); | |
| 1662 if (!resolved.is_valid() || resolved.GetOrigin() != url()) { | |
| 1663 *error = ErrorUtils::FormatErrorMessageUTF16( | |
| 1664 errors::kInvalidLaunchValue, | |
| 1665 keys::kLaunchLocalPath); | |
| 1666 return false; | |
| 1667 } | |
| 1668 | |
| 1669 launch_local_path_ = launch_path; | |
| 1670 } else if (manifest_->Get(keys::kLaunchWebURL, &temp)) { | |
| 1671 std::string launch_url; | |
| 1672 if (!temp->GetAsString(&launch_url)) { | |
| 1673 *error = ErrorUtils::FormatErrorMessageUTF16( | |
| 1674 errors::kInvalidLaunchValue, | |
| 1675 keys::kLaunchWebURL); | |
| 1676 return false; | |
| 1677 } | |
| 1678 | |
| 1679 // Ensure the launch URL is a valid absolute URL and web extent scheme. | |
| 1680 GURL url(launch_url); | |
| 1681 URLPattern pattern(kValidWebExtentSchemes); | |
| 1682 if (!url.is_valid() || !pattern.SetScheme(url.scheme())) { | |
| 1683 *error = ErrorUtils::FormatErrorMessageUTF16( | |
| 1684 errors::kInvalidLaunchValue, | |
| 1685 keys::kLaunchWebURL); | |
| 1686 return false; | |
| 1687 } | |
| 1688 | |
| 1689 launch_web_url_ = launch_url; | |
| 1690 } else if (is_legacy_packaged_app() || is_hosted_app()) { | |
| 1691 *error = ASCIIToUTF16(errors::kLaunchURLRequired); | |
| 1692 return false; | |
| 1693 } | |
| 1694 | |
| 1695 // If there is no extent, we default the extent based on the launch URL. | |
| 1696 if (web_extent().is_empty() && !launch_web_url().empty()) { | |
| 1697 GURL launch_url(launch_web_url()); | |
| 1698 URLPattern pattern(kValidWebExtentSchemes); | |
| 1699 if (!pattern.SetScheme("*")) { | |
| 1700 *error = ErrorUtils::FormatErrorMessageUTF16( | |
| 1701 errors::kInvalidLaunchValue, | |
| 1702 keys::kLaunchWebURL); | |
| 1703 return false; | |
| 1704 } | |
| 1705 pattern.SetHost(launch_url.host()); | |
| 1706 pattern.SetPath("/*"); | |
| 1707 extent_.AddPattern(pattern); | |
| 1708 } | |
| 1709 | |
| 1710 // In order for the --apps-gallery-url switch to work with the gallery | |
| 1711 // process isolation, we must insert any provided value into the component | |
| 1712 // app's launch url and web extent. | |
| 1713 if (id() == extension_misc::kWebStoreAppId) { | |
| 1714 std::string gallery_url_str = CommandLine::ForCurrentProcess()-> | |
| 1715 GetSwitchValueASCII(switches::kAppsGalleryURL); | |
| 1716 | |
| 1717 // Empty string means option was not used. | |
| 1718 if (!gallery_url_str.empty()) { | |
| 1719 GURL gallery_url(gallery_url_str); | |
| 1720 OverrideLaunchUrl(gallery_url); | |
| 1721 } | |
| 1722 } else if (id() == extension_misc::kCloudPrintAppId) { | |
| 1723 // In order for the --cloud-print-service switch to work, we must update | |
| 1724 // the launch URL and web extent. | |
| 1725 // TODO(sanjeevr): Ideally we want to use CloudPrintURL here but that is | |
| 1726 // currently under chrome/browser. | |
| 1727 const CommandLine& command_line = *CommandLine::ForCurrentProcess(); | |
| 1728 GURL cloud_print_service_url = GURL(command_line.GetSwitchValueASCII( | |
| 1729 switches::kCloudPrintServiceURL)); | |
| 1730 if (!cloud_print_service_url.is_empty()) { | |
| 1731 std::string path( | |
| 1732 cloud_print_service_url.path() + "/enable_chrome_connector"); | |
| 1733 GURL::Replacements replacements; | |
| 1734 replacements.SetPathStr(path); | |
| 1735 GURL cloud_print_enable_connector_url = | |
| 1736 cloud_print_service_url.ReplaceComponents(replacements); | |
| 1737 OverrideLaunchUrl(cloud_print_enable_connector_url); | |
| 1738 } | |
| 1739 } else if (id() == extension_misc::kChromeAppId) { | |
| 1740 // Override launch url to new tab. | |
| 1741 launch_web_url_ = chrome::kChromeUINewTabURL; | |
| 1742 extent_.ClearPatterns(); | |
| 1743 } | |
| 1744 | |
| 1745 return true; | |
| 1746 } | |
| 1747 | |
| 1748 bool Extension::LoadSharedFeatures(string16* error) { | 1561 bool Extension::LoadSharedFeatures(string16* error) { |
| 1749 if (!LoadDescription(error) || | 1562 if (!LoadDescription(error) || |
| 1750 !ManifestHandler::ParseExtension(this, error) || | 1563 !ManifestHandler::ParseExtension(this, error) || |
| 1751 !LoadNaClModules(error) || | 1564 !LoadNaClModules(error) || |
| 1752 !LoadSandboxedPages(error) || | 1565 !LoadSandboxedPages(error) || |
| 1753 !LoadRequirements(error) || | 1566 !LoadRequirements(error) || |
| 1754 !LoadKioskEnabled(error) || | 1567 !LoadKioskEnabled(error) || |
| 1755 !LoadOfflineEnabled(error)) | 1568 !LoadOfflineEnabled(error)) |
| 1756 return false; | 1569 return false; |
| 1757 | 1570 |
| (...skipping 588 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 2346 | 2159 |
| 2347 if (ActionInfo::GetBrowserActionInfo(this)) | 2160 if (ActionInfo::GetBrowserActionInfo(this)) |
| 2348 ++num_surfaces; | 2161 ++num_surfaces; |
| 2349 | 2162 |
| 2350 if (is_app()) | 2163 if (is_app()) |
| 2351 ++num_surfaces; | 2164 ++num_surfaces; |
| 2352 | 2165 |
| 2353 return num_surfaces > 1; | 2166 return num_surfaces > 1; |
| 2354 } | 2167 } |
| 2355 | 2168 |
| 2356 void Extension::OverrideLaunchUrl(const GURL& override_url) { | |
| 2357 GURL new_url(override_url); | |
| 2358 if (!new_url.is_valid()) { | |
| 2359 DLOG(WARNING) << "Invalid override url given for " << name(); | |
| 2360 } else { | |
| 2361 if (new_url.has_port()) { | |
| 2362 DLOG(WARNING) << "Override URL passed for " << name() | |
| 2363 << " should not contain a port. Removing it."; | |
| 2364 | |
| 2365 GURL::Replacements remove_port; | |
| 2366 remove_port.ClearPort(); | |
| 2367 new_url = new_url.ReplaceComponents(remove_port); | |
| 2368 } | |
| 2369 | |
| 2370 launch_web_url_ = new_url.spec(); | |
| 2371 | |
| 2372 URLPattern pattern(kValidWebExtentSchemes); | |
| 2373 URLPattern::ParseResult result = pattern.Parse(new_url.spec()); | |
| 2374 DCHECK_EQ(result, URLPattern::PARSE_SUCCESS); | |
| 2375 pattern.SetPath(pattern.path() + '*'); | |
| 2376 extent_.AddPattern(pattern); | |
| 2377 } | |
| 2378 } | |
| 2379 | |
| 2380 bool Extension::CanSpecifyExperimentalPermission() const { | 2169 bool Extension::CanSpecifyExperimentalPermission() const { |
| 2381 if (location() == Manifest::COMPONENT) | 2170 if (location() == Manifest::COMPONENT) |
| 2382 return true; | 2171 return true; |
| 2383 | 2172 |
| 2384 if (CommandLine::ForCurrentProcess()->HasSwitch( | 2173 if (CommandLine::ForCurrentProcess()->HasSwitch( |
| 2385 switches::kEnableExperimentalExtensionApis)) { | 2174 switches::kEnableExperimentalExtensionApis)) { |
| 2386 return true; | 2175 return true; |
| 2387 } | 2176 } |
| 2388 | 2177 |
| 2389 // We rely on the webstore to check access to experimental. This way we can | 2178 // We rely on the webstore to check access to experimental. This way we can |
| (...skipping 123 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 2513 | 2302 |
| 2514 UpdatedExtensionPermissionsInfo::UpdatedExtensionPermissionsInfo( | 2303 UpdatedExtensionPermissionsInfo::UpdatedExtensionPermissionsInfo( |
| 2515 const Extension* extension, | 2304 const Extension* extension, |
| 2516 const PermissionSet* permissions, | 2305 const PermissionSet* permissions, |
| 2517 Reason reason) | 2306 Reason reason) |
| 2518 : reason(reason), | 2307 : reason(reason), |
| 2519 extension(extension), | 2308 extension(extension), |
| 2520 permissions(permissions) {} | 2309 permissions(permissions) {} |
| 2521 | 2310 |
| 2522 } // namespace extensions | 2311 } // namespace extensions |
| OLD | NEW |