chrome-devtools-frontend
Version:
Chrome DevTools UI
255 lines (236 loc) • 11.1 kB
text/typescript
// Copyright 2021 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.
import * as i18n from '../../../core/i18n/i18n.js';
import * as SDK from '../../../core/sdk/sdk.js';
import * as Protocol from '../../../generated/protocol.js';
import * as NetworkForward from '../../../panels/network/forward/forward.js';
import * as ComponentHelpers from '../../../ui/components/helpers/helpers.js';
import * as IconButton from '../../../ui/components/icon_button/icon_button.js';
import * as Coordinator from '../../../ui/components/render_coordinator/render_coordinator.js';
import * as ReportView from '../../../ui/components/report_view/report_view.js';
import * as LitHtml from '../../../ui/lit-html/lit-html.js';
import permissionsPolicySectionStyles from './permissionsPolicySection.css.js';
import type * as Platform from '../../../core/platform/platform.js';
import * as Common from '../../../core/common/common.js';
const UIStrings = {
/**
*@description Label for a button. When clicked more details (for the content this button refers to) will be shown.
*/
showDetails: 'Show details',
/**
*@description Label for a button. When clicked some details (for the content this button refers to) will be hidden.
*/
hideDetails: 'Hide details',
/**
*@description Label for a list of features which are allowed according to the current Permissions policy
*(a mechanism that allows developers to enable/disable browser features and APIs (e.g. camera, geolocation, autoplay))
*/
allowedFeatures: 'Allowed Features',
/**
*@description Label for a list of features which are disabled according to the current Permissions policy
*(a mechanism that allows developers to enable/disable browser features and APIs (e.g. camera, geolocation, autoplay))
*/
disabledFeatures: 'Disabled Features',
/**
*@description Tooltip text for a link to a specific request's headers in the Network panel.
*/
clickToShowHeader: 'Click to reveal the request whose "`Permissions-Policy`" HTTP header disables this feature.',
/**
*@description Tooltip text for a link to a specific iframe in the Elements panel (Iframes can be nested, the link goes
* to the outer-most iframe which blocks a certain feature).
*/
clickToShowIframe: 'Click to reveal the top-most iframe which does not allow this feature in the elements panel.',
/**
*@description Text describing that a specific feature is blocked by not being included in the iframe's "allow" attribute.
*/
disabledByIframe: 'missing in iframe "`allow`" attribute',
/**
*@description Text describing that a specific feature is blocked by a Permissions Policy specified in a request header.
*/
disabledByHeader: 'disabled by "`Permissions-Policy`" header',
/**
*@description Text describing that a specific feature is blocked by virtue of being inside a fenced frame tree.
*/
disabledByFencedFrame: 'disabled inside a `fencedframe`',
};
const str_ = i18n.i18n.registerUIStrings('panels/application/components/PermissionsPolicySection.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
const coordinator = Coordinator.RenderCoordinator.RenderCoordinator.instance();
export interface PermissionsPolicySectionData {
policies: Protocol.Page.PermissionsPolicyFeatureState[];
showDetails: boolean;
}
export function renderIconLink(
iconName: string, title: Platform.UIString.LocalizedString,
clickHandler: (() => void)|(() => Promise<void>)): LitHtml.TemplateResult {
// Disabled until https://crbug.com/1079231 is fixed.
// clang-format off
return LitHtml.html`
<button class="link" role="link" tabindex=0 @click=${clickHandler} title=${title}>
<${IconButton.Icon.Icon.litTagName} .data=${{
iconName: iconName,
color: 'var(--icon-link)',
width: '16px',
height: '16px',
} as IconButton.Icon.IconData}>
</${IconButton.Icon.Icon.litTagName}>
</button>
`;
// clang-format on
}
export class PermissionsPolicySection extends HTMLElement {
static readonly litTagName = LitHtml.literal`devtools-resources-permissions-policy-section`;
readonly #shadow = this.attachShadow({mode: 'open'});
#permissionsPolicySectionData: PermissionsPolicySectionData = {policies: [], showDetails: false};
set data(data: PermissionsPolicySectionData) {
this.#permissionsPolicySectionData = data;
void this.#render();
}
connectedCallback(): void {
this.#shadow.adoptedStyleSheets = [permissionsPolicySectionStyles];
}
#toggleShowPermissionsDisallowedDetails(): void {
this.#permissionsPolicySectionData.showDetails = !this.#permissionsPolicySectionData.showDetails;
void this.#render();
}
#renderAllowed(): LitHtml.LitTemplate {
const allowed = this.#permissionsPolicySectionData.policies.filter(p => p.allowed).map(p => p.feature).sort();
if (!allowed.length) {
return LitHtml.nothing;
}
return LitHtml.html`
<${ReportView.ReportView.ReportKey.litTagName}>${i18nString(UIStrings.allowedFeatures)}</${
ReportView.ReportView.ReportKey.litTagName}>
<${ReportView.ReportView.ReportValue.litTagName}>
${allowed.join(', ')}
</${ReportView.ReportView.ReportValue.litTagName}>
`;
}
async #renderDisallowed(): Promise<LitHtml.LitTemplate> {
const disallowed = this.#permissionsPolicySectionData.policies.filter(p => !p.allowed)
.sort((a, b) => a.feature.localeCompare(b.feature));
if (!disallowed.length) {
return LitHtml.nothing;
}
if (!this.#permissionsPolicySectionData.showDetails) {
return LitHtml.html`
<${ReportView.ReportView.ReportKey.litTagName}>${i18nString(UIStrings.disabledFeatures)}</${
ReportView.ReportView.ReportKey.litTagName}>
<${ReportView.ReportView.ReportValue.litTagName}>
${disallowed.map(p => p.feature).join(', ')}
<button class="link" @click=${(): void => this.#toggleShowPermissionsDisallowedDetails()}>
${i18nString(UIStrings.showDetails)}
</button>
</${ReportView.ReportView.ReportValue.litTagName}>
`;
}
const frameManager = SDK.FrameManager.FrameManager.instance();
const featureRows = await Promise.all(disallowed.map(async policy => {
const frame = policy.locator ? frameManager.getFrame(policy.locator.frameId) : null;
const blockReason = policy.locator?.blockReason;
const linkTargetDOMNode = await (
blockReason === Protocol.Page.PermissionsPolicyBlockReason.IframeAttribute && frame &&
frame.getOwnerDOMNodeOrDocument());
const resource = frame && frame.resourceForURL(frame.url);
const linkTargetRequest =
blockReason === Protocol.Page.PermissionsPolicyBlockReason.Header && resource && resource.request;
const blockReasonText = ((): String => {
switch (blockReason) {
case Protocol.Page.PermissionsPolicyBlockReason.IframeAttribute:
return i18nString(UIStrings.disabledByIframe);
case Protocol.Page.PermissionsPolicyBlockReason.Header:
return i18nString(UIStrings.disabledByHeader);
case Protocol.Page.PermissionsPolicyBlockReason.InFencedFrameTree:
return i18nString(UIStrings.disabledByFencedFrame);
default:
return '';
}
})();
const revealHeader = async(): Promise<void> => {
if (!linkTargetRequest) {
return;
}
const headerName =
linkTargetRequest.responseHeaderValue('permissions-policy') ? 'permissions-policy' : 'feature-policy';
const requestLocation = NetworkForward.UIRequestLocation.UIRequestLocation.responseHeaderMatch(
linkTargetRequest,
{name: headerName, value: ''},
);
await Common.Revealer.reveal(requestLocation);
};
// Disabled until https://crbug.com/1079231 is fixed.
// clang-format off
return LitHtml.html`
<div class="permissions-row">
<div>
<${IconButton.Icon.Icon.litTagName} class="allowed-icon"
.data=${{
color: 'var(--icon-error)',
iconName: 'cross-circle',
width: '20px', height: '20px',
} as IconButton.Icon.IconData}>
</${IconButton.Icon.Icon.litTagName}>
</div>
<div class="feature-name text-ellipsis">
${policy.feature}
</div>
<div class="block-reason">${blockReasonText}</div>
<div>
${
linkTargetDOMNode ? renderIconLink(
'code-circle', i18nString(UIStrings.clickToShowIframe),
(): Promise<void> => Common.Revealer.reveal(linkTargetDOMNode)) :
LitHtml.nothing}
${
linkTargetRequest ? renderIconLink(
'arrow-up-down-circle',
i18nString(UIStrings.clickToShowHeader),
revealHeader,
) :
LitHtml.nothing}
</div>
</div>
`;
// clang-format on
}));
return LitHtml.html`
<${ReportView.ReportView.ReportKey.litTagName}>${i18nString(UIStrings.disabledFeatures)}</${
ReportView.ReportView.ReportKey.litTagName}>
<${ReportView.ReportView.ReportValue.litTagName} class="policies-list">
${featureRows}
<div class="permissions-row">
<button class="link" @click=${(): void => this.#toggleShowPermissionsDisallowedDetails()}>
${i18nString(UIStrings.hideDetails)}
</button>
</div>
</${ReportView.ReportView.ReportValue.litTagName}>
`;
}
async #render(): Promise<void> {
await coordinator.write('PermissionsPolicySection render', () => {
// Disabled until https://crbug.com/1079231 is fixed.
// clang-format off
LitHtml.render(
LitHtml.html`
<${ReportView.ReportView.ReportSectionHeader.litTagName}>${i18n.i18n.lockedString('Permissions Policy')}</${
ReportView.ReportView.ReportSectionHeader.litTagName}>
${this.#renderAllowed()}
${LitHtml.Directives.until(this.#renderDisallowed(), LitHtml.nothing)}
<${ReportView.ReportView.ReportSectionDivider.litTagName}></${
ReportView.ReportView.ReportSectionDivider.litTagName}>
`,
this.#shadow, {host: this},
);
// clang-format on
});
}
}
ComponentHelpers.CustomElements.defineComponent(
'devtools-resources-permissions-policy-section', PermissionsPolicySection);
declare global {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface HTMLElementTagNameMap {
'devtools-resources-permissions-policy-section': PermissionsPolicySection;
}
}