Commit 0ba08a46 authored by Devlin Cronin's avatar Devlin Cronin Committed by Commit Bot

[Extensions Click-to-Script] Create a toggle-able list for specific sites

For extensions that only request access to specific sites (e.g., an
extension that only requests access to google.com and example.com),
display a toggle-able list in the chrome://extensions page rather than
a free-form entry field. The ability to grant all requested sites
still exists via a "main" toggle, and disabling this allows the user to
toggle individual sites. This logic is contained in the new
HostPermissionsToggleList custom element.

This is a usability improvement over the free-form entry, since with
free-form entry it's not immediately obvious what the possible options
are.

Free-form entry is still used for extensions that request all sites (or
all-sites like patterns, such as https://*.com), since (almost) any site
entered is valid.

Update and add tests for the same.

Bug: 891803

Cq-Include-Trybots: luci.chromium.try:closure_compilation
Change-Id: I99112966d323c403603d5f21c28ae33be8e506f0
Reviewed-on: https://chromium-review.googlesource.com/c/1284696
Commit-Queue: Devlin <rdevlin.cronin@chromium.org>
Reviewed-by: default avatarDemetrios Papadopoulos <dpapad@chromium.org>
Cr-Commit-Position: refs/heads/master@{#605150}
parent 735e1bc2
......@@ -56,6 +56,7 @@ js_type_check("closure_compile") {
":drag_and_drop_handler",
":drop_overlay",
":error_page",
":host_permissions_toggle_list",
":install_warnings_dialog",
":item",
":item_behavior",
......@@ -145,6 +146,13 @@ js_library("error_page") {
]
}
js_library("host_permissions_toggle_list") {
deps = [
"//ui/webui/resources/js:cr",
]
externs_list = [ "$externs_path/developer_private.js" ]
}
js_library("install_warnings_dialog") {
deps = [
"//ui/webui/resources/js:cr",
......
......@@ -20,6 +20,7 @@
<link rel="import" href="chrome://resources/polymer/v1_0/paper-styles/color.html">
<link rel="import" href="item_behavior.html">
<link rel="import" href="item_util.html">
<link rel="import" href="host_permissions_toggle_list.html">
<link rel="import" href="navigation_helper.html">
<link rel="import" href="runtime_host_permissions.html">
<link rel="import" href="strings.html">
......@@ -166,6 +167,13 @@
border-top: var(--cr-separator-line);
}
extensions-toggle-row {
@apply --cr-section;
padding-inline-end: 0;
padding-inline-start: 0;
--toggle-row-label-padding: var(--cr-section-padding);
}
#load-path {
word-break: break-all;
}
......@@ -329,13 +337,22 @@
</li>
</template>
</ul>
<template is="dom-if" if="[[hasRuntimeHostPermissions_(data.*)]]">
<template is="dom-if"
if="[[showFreeformRuntimeHostPermissions_(data.*)]]">
<extensions-runtime-host-permissions
permissions="[[data.permissions.runtimeHostPermissions]]"
delegate="[[delegate]]"
item-id="[[data.id]]">
</extensions-runtime-host-permissions>
</template>
<template is="dom-if"
if="[[showHostPermissionsToggleList_(data.*)]]">
<extensions-host-permissions-toggle-list
permissions="[[data.permissions.runtimeHostPermissions]]"
delegate="[[delegate]]"
item-id="[[data.id]]">
</extensions-host-permissions-toggle-list>
</template>
</div>
</div>
<template is="dom-if"
......
......@@ -302,6 +302,24 @@ cr.define('extensions', function() {
hasRuntimeHostPermissions_: function() {
return !!this.data.permissions.runtimeHostPermissions;
},
/**
* @return {boolean}
* @private
*/
showFreeformRuntimeHostPermissions_: function() {
return this.hasRuntimeHostPermissions_() &&
this.data.permissions.runtimeHostPermissions.hasAllHosts;
},
/**
* @return {boolean}
* @private
*/
showHostPermissionsToggleList_: function() {
return this.hasRuntimeHostPermissions_() &&
!this.data.permissions.runtimeHostPermissions.hasAllHosts;
},
});
return {DetailView: DetailView};
......
......@@ -123,6 +123,12 @@
<structure name="IDR_MD_EXTENSIONS_LOAD_ERROR_JS"
file="load_error.js"
type="chrome_html" />
<structure name="IDR_MD_EXTENSIONS_HOST_PERMISSIONS_TOGGLE_LIST_HMTL"
file="host_permissions_toggle_list.html"
type="chrome_html" />
<structure name="IDR_MD_EXTENSIONS_HOST_PERMISSIONS_TOGGLE_LIST_JS"
file="host_permissions_toggle_list.js"
type="chrome_html" />
<structure name="IDR_MD_EXTENSIONS_NAVIGATION_HELPER_HTML"
file="navigation_helper.html"
type="chrome_html" />
......
<link rel="import" href="chrome://resources/html/polymer.html">
<link rel="import" href="chrome://resources/cr_elements/shared_style_css.html">
<link rel="import" href="chrome://resources/cr_elements/shared_vars_css.html">
<link rel="import" href="chrome://resources/html/cr.html">
<link rel="import" href="toggle_row.html">
<link rel="import" href="strings.html">
<dom-module id="extensions-host-permissions-toggle-list">
<template>
<style include="cr-shared-style">
#section-heading {
color: var(--cr-primary-text-color);
margin-top: 12px;
}
a[href] {
color: var(--google-blue-700);
text-decoration: none;
}
extensions-toggle-row {
color: black;
}
.toggle-section {
display: flex;
flex-direction: column;
justify-content: center;
min-height: var(--cr-section-min-height);
}
.site-toggle {
border-top: var(--cr-separator-line);
margin-inline-start: var(--cr-section-indent-width);
}
</style>
<div id="section-heading">$i18n{hostPermissionsHeading}</div>
<div class="toggle-section">
<extensions-toggle-row checked="[[allowedOnAllHosts_(permissions.*)]]"
id="allHostsToggle"
on-change="onAllHostsToggleChanged_">
<span>$i18n{itemAllowOnAllSites}</span>
</extensions-toggle-row>
</div>
<template is="dom-repeat" items="[[getSortedHosts_(permissions.*)]]">
<div class="toggle-section site-toggle">
<extensions-toggle-row checked="[[item.granted]]"
class="host-toggle no-end-padding"
disabled="[[allowedOnAllHosts_(permissions.*)]]"
host="[[item.host]]"
on-change="onHostAccessChanged_">
<span>[[item.host]]</span>
</extensions-toggle-row>
</div>
</template>
<div>$i18nRaw{hostPermissionsLearnMoreLink}</div>
</template>
<script src="host_permissions_toggle_list.js"></script>
</dom-module>
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
cr.define('extensions', function() {
'use strict';
const HostPermissionsToggleList = Polymer({
is: 'extensions-host-permissions-toggle-list',
properties: {
/**
* The underlying permissions data.
* @type {chrome.developerPrivate.RuntimeHostPermissions}
*/
permissions: Object,
/** @private */
itemId: String,
/** @type {!extensions.ItemDelegate} */
delegate: Object,
},
/**
* @return {boolean} Whether the item is allowed to execute on all of its
* requested sites.
* @private
*/
allowedOnAllHosts_: function() {
return this.permissions.hostAccess ==
chrome.developerPrivate.HostAccess.ON_ALL_SITES;
},
/**
* Returns a lexicographically-sorted list of the hosts associated with this
* item.
* @return {!Array<!chrome.developerPrivate.SiteControl>}
* @private
*/
getSortedHosts_: function() {
return this.permissions.hosts.sort((a, b) => {
if (a.host < b.host)
return -1;
if (a.host > b.host)
return 1;
return 0;
});
},
/** @private */
onAllHostsToggleChanged_: function() {
// TODO(devlin): In the case of going from all sites to specific sites,
// we'll withhold all sites (i.e., all specific site toggles will move to
// unchecked, and the user can check them individually). This is slightly
// different than the sync page, where disabling the "sync everything"
// switch leaves everything synced, and user can uncheck them
// individually. It could be nice to align on behavior, but probably not
// super high priority.
this.delegate.setItemHostAccess(
this.itemId,
this.$.allHostsToggle.checked ?
chrome.developerPrivate.HostAccess.ON_ALL_SITES :
chrome.developerPrivate.HostAccess.ON_SPECIFIC_SITES);
},
/** @private */
onHostAccessChanged_: function(e) {
const host = e.target.host;
const checked = e.target.checked;
if (checked)
this.delegate.addRuntimeHostPermission(this.itemId, host);
else
this.delegate.removeRuntimeHostPermission(this.itemId, host);
},
});
return {HostPermissionsToggleList: HostPermissionsToggleList};
});
......@@ -7,10 +7,7 @@
<template>
<style>
:host {
@apply --cr-section;
flex-direction: column;
padding-left: 0;
padding-right: 0;
touch-action: none;
}
......@@ -24,7 +21,7 @@
cursor: pointer;
display: flex;
flex: 1;
padding: 0 var(--cr-section-padding);
padding: 0 var(--toggle-row-label-padding, 0);
width: 100%;
}
......@@ -39,10 +36,11 @@
</style>
<label id="label">
<input id="native" type="checkbox" checked="[[checked]]"
on-change="onNativeChange_" on-click="onNativeClick_">
on-change="onNativeChange_" on-click="onNativeClick_"
disabled="[[disabled]]">
<slot></slot>
<cr-toggle id="crToggle" checked="{{checked}}" aria-labelledby="label"
on-change="onCrToggleChange_"></cr-toggle>
on-change="onCrToggleChange_" disabled="[[disabled]]"></cr-toggle>
</label>
</template>
<script src="toggle_row.js"></script>
......
......@@ -15,6 +15,8 @@ cr.define('extensions', function() {
properties: {
checked: Boolean,
disabled: Boolean,
},
/**
......
......@@ -872,3 +872,24 @@ CrExtensionsRuntimeHostPermissionsTest = class extends CrExtensionsBrowserTest {
TEST_F('CrExtensionsRuntimeHostPermissionsTest', 'All', () => {
mocha.run();
});
////////////////////////////////////////////////////////////////////////////////
// HostPermissionsToggleList tests
CrExtensionsHostPermissionsToggleListTest =
class extends CrExtensionsBrowserTest {
/** @override */
get browserPreload() {
return 'chrome://extensions/host_permissions_toggle_list.html';
}
get extraLibraries() {
return super.extraLibraries.concat([
'host_permissions_toggle_list_test.js',
]);
}
};
TEST_F('CrExtensionsHostPermissionsToggleListTest', 'All', () => {
mocha.run();
});
......@@ -165,6 +165,11 @@ cr.define('extension_detail_view_tests', function() {
Polymer.dom.flush();
expectTrue(testIsVisible('.warning-icon'));
// Ensure that without runtimeHostPermissions data, the sections are
// hidden.
expectFalse(testIsVisible('extensions-runtime-host-permissions'));
expectFalse(testIsVisible('extensions-host-permissions-toggle-list'));
// Adding any runtime host permissions should result in the runtime host
// controls becoming visible.
const allSitesPermissions = {
......@@ -178,6 +183,23 @@ cr.define('extension_detail_view_tests', function() {
item.set('data.permissions', allSitesPermissions);
Polymer.dom.flush();
expectTrue(testIsVisible('extensions-runtime-host-permissions'));
expectFalse(testIsVisible('extensions-host-permissions-toggle-list'));
const someSitesPermissions = {
simplePermissions: [],
runtimeHostPermissions: {
hosts: [
{granted: true, host: 'https://chromium.org/*'},
{granted: false, host: 'https://example.com/*'}
],
hasAllHosts: false,
hostAccess: chrome.developerPrivate.HostAccess.ON_SPECIFIC_SITES,
},
};
item.set('data.permissions', someSitesPermissions);
Polymer.dom.flush();
expectFalse(testIsVisible('extensions-runtime-host-permissions'));
expectTrue(testIsVisible('extensions-host-permissions-toggle-list'));
});
test(assert(TestNames.LayoutSource), function() {
......
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
suite('HostPermissionsToggleList', function() {
/** @type {extensions.HostPermissionsToggleListElement} */ let element;
/** @type {extensions.TestService} */ let delegate;
const HostAccess = chrome.developerPrivate.HostAccess;
const ITEM_ID = 'a'.repeat(32);
const EXAMPLE_COM = 'https://example.com/*';
const GOOGLE_COM = 'https://google.com/*';
const CHROMIUM_ORG = 'https://chromium.org/*';
setup(function() {
PolymerTest.clearBody();
element = document.createElement('extensions-host-permissions-toggle-list');
delegate = new extensions.TestService();
element.delegate = delegate;
element.itemId = ITEM_ID;
document.body.appendChild(element);
});
teardown(function() {
element.remove();
});
// Tests the display of the list when only specific sites are granted.
test('permissions display for specific sites', function() {
const permissions = {
hostAccess: HostAccess.ON_SPECIFIC_SITES,
hasAllHosts: false,
hosts: [
{host: EXAMPLE_COM, granted: true},
{host: GOOGLE_COM, granted: false},
{host: CHROMIUM_ORG, granted: true},
],
};
element.permissions = permissions;
Polymer.dom.flush();
assertTrue(!!element.$);
const allSites = element.$.allHostsToggle;
expectFalse(allSites.checked);
const hostToggles = element.shadowRoot.querySelectorAll('.host-toggle');
assertEquals(3, hostToggles.length);
// There should be three toggles, all enabled, and checked corresponding to
// whether the host is granted.
expectEquals(CHROMIUM_ORG, hostToggles[0].innerText.trim());
expectFalse(hostToggles[0].disabled);
expectTrue(hostToggles[0].checked);
expectEquals(EXAMPLE_COM, hostToggles[1].innerText.trim());
expectFalse(hostToggles[1].disabled);
expectTrue(hostToggles[1].checked);
expectEquals(GOOGLE_COM, hostToggles[2].innerText.trim());
expectFalse(hostToggles[2].disabled);
expectFalse(hostToggles[2].checked);
});
// Tests the display when the user has chosen to allow on all the requested
// sites.
test('permissions display for all requested sites', function() {
const permissions = {
hostAccess: HostAccess.ON_ALL_SITES,
hasAllHosts: false,
hosts: [
{host: EXAMPLE_COM, granted: true},
{host: GOOGLE_COM, granted: true},
{host: CHROMIUM_ORG, granted: true},
],
};
element.permissions = permissions;
Polymer.dom.flush();
assertTrue(!!element.$);
const allSites = element.$.allHostsToggle;
expectTrue(allSites.checked);
const hostToggles = element.shadowRoot.querySelectorAll('.host-toggle');
assertEquals(3, hostToggles.length);
// There should be three toggles, and they should all be disabled and
// checked, since the user selected to allow the extension to run on all
// (requested) sites.
expectEquals(CHROMIUM_ORG, hostToggles[0].innerText.trim());
expectTrue(hostToggles[0].disabled);
expectTrue(hostToggles[0].checked);
expectEquals(EXAMPLE_COM, hostToggles[1].innerText.trim());
expectTrue(hostToggles[1].disabled);
expectTrue(hostToggles[1].checked);
expectEquals(GOOGLE_COM, hostToggles[2].innerText.trim());
expectTrue(hostToggles[2].disabled);
expectTrue(hostToggles[2].checked);
});
// Tests the permissions display when a user has chosen to only run an
// extension on-click.
test('permissions display for on click', function() {
const permissions = {
hostAccess: HostAccess.ON_CLICK,
hasAllHosts: false,
hosts: [
{host: EXAMPLE_COM, granted: false},
{host: GOOGLE_COM, granted: false},
{host: CHROMIUM_ORG, granted: false},
],
};
element.permissions = permissions;
Polymer.dom.flush();
assertTrue(!!element.$);
const allSites = element.$.allHostsToggle;
expectFalse(allSites.checked);
const hostToggles = element.shadowRoot.querySelectorAll('.host-toggle');
assertEquals(3, hostToggles.length);
// There should be three toggles, all enabled, and all unchecked, since no
// host has been granted.
expectEquals(CHROMIUM_ORG, hostToggles[0].innerText.trim());
expectFalse(hostToggles[0].disabled);
expectFalse(hostToggles[0].checked);
expectEquals(EXAMPLE_COM, hostToggles[1].innerText.trim());
expectFalse(hostToggles[1].disabled);
expectFalse(hostToggles[1].checked);
expectEquals(GOOGLE_COM, hostToggles[2].innerText.trim());
expectFalse(hostToggles[2].disabled);
expectFalse(hostToggles[2].checked);
});
// Tests that clicking the "allow on all sites" toggle changes the item
// host access properly.
test('clicking all hosts toggle', function() {
const permissions = {
hostAccess: HostAccess.ON_CLICK,
hasAllHosts: false,
hosts: [
{host: EXAMPLE_COM, granted: false},
{host: GOOGLE_COM, granted: false},
{host: CHROMIUM_ORG, granted: false},
],
};
element.permissions = permissions;
Polymer.dom.flush();
assertTrue(!!element.$);
const allSites = element.$.allHostsToggle;
allSites.getLabel().click();
return delegate.whenCalled('setItemHostAccess').then((args) => {
expectEquals(ITEM_ID, args[0] /* id */);
expectEquals(HostAccess.ON_ALL_SITES, args[1] /* access */);
});
});
// Tests that toggling a site's enabled state toggles the extension's access
// to that site properly.
test('clicking to toggle a specific site', function() {
const permissions = {
hostAccess: HostAccess.ON_SPECIFIC_SITES,
hasAllHosts: false,
hosts: [
{host: EXAMPLE_COM, granted: true},
{host: GOOGLE_COM, granted: false},
{host: CHROMIUM_ORG, granted: true},
],
};
element.permissions = permissions;
Polymer.dom.flush();
const hostToggles = element.shadowRoot.querySelectorAll('.host-toggle');
assertEquals(3, hostToggles.length);
expectEquals(CHROMIUM_ORG, hostToggles[0].innerText.trim());
expectEquals(GOOGLE_COM, hostToggles[2].innerText.trim());
hostToggles[0].getLabel().click();
return delegate.whenCalled('removeRuntimeHostPermission')
.then((args) => {
expectEquals(ITEM_ID, args[0] /* id */);
expectEquals(CHROMIUM_ORG, args[1] /* site */);
hostToggles[2].getLabel().click();
return delegate.whenCalled('addRuntimeHostPermission');
})
.then((args) => {
expectEquals(ITEM_ID, args[0] /* id */);
expectEquals(GOOGLE_COM, args[1] /* site */);
});
});
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment