Skip to content

Commit

Permalink
vsx-registry: implement verified extension filtering (#12995)
Browse files Browse the repository at this point in the history
This commit handles the contribution of the 'verified' field when
searching for extensions in the vsx-registry. It implements a checkmark
near the publisher name when the extension's publisher is verified.
Adds a toggle button near the search bar in the vsx
registry which allows for the filtering of extensions. When toggled,
only verified extensions will be shown during the search which allows
the user to quickly filter out extensions that might be deemed unsafe.

Signed-off-by: Vlad Arama <vlad.arama@ericsson.com>
  • Loading branch information
vladarama authored Jan 15, 2024
1 parent e0d4983 commit abc49cd
Show file tree
Hide file tree
Showing 8 changed files with 284 additions and 40 deletions.
1 change: 1 addition & 0 deletions dev-packages/ovsx-client/src/ovsx-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ export interface VSXExtensionRaw {
version: string;
timestamp: string;
preview?: boolean;
verified?: boolean;
displayName?: string;
description?: string;
categories?: string[];
Expand Down
67 changes: 65 additions & 2 deletions packages/vsx-registry/src/browser/style/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,56 @@
);
}

.vsx-search-container {
display: flex;
align-items: center;
width: 100%;
background: var(--theia-input-background);
border-style: solid;
border-width: var(--theia-border-width);
border-color: var(--theia-input-background);
border-radius: 2px;
}

.vsx-search-container:focus-within {
border-color: var(--theia-focusBorder);
}

.vsx-search-container .option-buttons {
height: 23px;
display: flex;
align-items: center;
align-self: flex-start;
background-color: none;
margin: 2px;
}

.vsx-search-container .option {
width: 21px;
height: 21px;
margin: 0 1px;
display: inline-block;
box-sizing: border-box;
align-items: center;
user-select: none;
background-repeat: no-repeat;
background-position: center;
border: var(--theia-border-width) solid transparent;
opacity: 0.7;
cursor: pointer;
}

.vsx-search-container .option.enabled {
color: var(--theia-inputOption-activeForeground);
border: var(--theia-border-width) var(--theia-inputOption-activeBorder) solid;
background-color: var(--theia-inputOption-activeBackground);
opacity: 1;
}

.vsx-search-container .option:hover {
opacity: 1;
}

.theia-vsx-extensions {
height: 100%;
}
Expand All @@ -42,10 +92,14 @@
overflow: hidden;
line-height: var(--theia-content-line-height);
flex: 1;
padding-top: calc(var(--theia-ui-padding) / 2);
padding-bottom: calc(var(--theia-ui-padding) / 2);
margin-top: calc(var(--theia-ui-padding) / 2);
margin-bottom: calc(var(--theia-ui-padding) / 2);
}

.theia-vsx-extensions-search-bar .theia-input:focus {
border: none;
outline: none;
}
.theia-vsx-extension {
display: flex;
flex-direction: row;
Expand Down Expand Up @@ -136,6 +190,15 @@
white-space: nowrap;
}

.theia-vsx-extension-action-bar .codicon-verified-filled {
color: var(--theia-extensionIcon-verifiedForeground);
margin-right: 2px;
}

.theia-vsx-extension-publisher-container {
display: flex;
}

.theia-vsx-extension-action-bar .action {
font-size: 90%;
min-width: auto !important;
Expand Down
49 changes: 39 additions & 10 deletions packages/vsx-registry/src/browser/vsx-extension.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import { Endpoint } from '@theia/core/lib/browser/endpoint';
import { VSXEnvironment } from '../common/vsx-environment';
import { VSXExtensionsSearchModel } from './vsx-extensions-search-model';
import { CommandRegistry, MenuPath, nls } from '@theia/core/lib/common';
import { codicon, ContextMenuRenderer, HoverService, TreeWidget } from '@theia/core/lib/browser';
import { codicon, ConfirmDialog, ContextMenuRenderer, HoverService, TreeWidget } from '@theia/core/lib/browser';
import { VSXExtensionNamespaceAccess, VSXUser } from '@theia/ovsx-client/lib/ovsx-types';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { MarkdownStringImpl } from '@theia/core/lib/common/markdown-rendering';
Expand Down Expand Up @@ -57,6 +57,7 @@ export class VSXExtensionData {
readonly license?: string;
readonly readme?: string;
readonly preview?: boolean;
readonly verified?: boolean;
readonly namespaceAccess?: VSXExtensionNamespaceAccess;
readonly publishedBy?: VSXUser;
static KEYS: Set<(keyof VSXExtensionData)> = new Set([
Expand All @@ -75,6 +76,7 @@ export class VSXExtensionData {
'license',
'readme',
'preview',
'verified',
'namespaceAccess',
'publishedBy'
]);
Expand Down Expand Up @@ -265,6 +267,10 @@ export class VSXExtension implements VSXExtensionData, TreeElement {
return this.getData('preview');
}

get verified(): boolean | undefined {
return this.getData('verified');
}

get namespaceAccess(): VSXExtensionNamespaceAccess | undefined {
return this.getData('namespaceAccess');
}
Expand Down Expand Up @@ -297,13 +303,16 @@ export class VSXExtension implements VSXExtensionData, TreeElement {
}

async install(options?: PluginDeployOptions): Promise<void> {
this._busy++;
try {
await this.progressService.withProgress(nls.localizeByDefault("Installing extension '{0}' v{1}...", this.id, this.version ?? 0), 'extensions', () =>
this.pluginServer.deploy(this.uri.toString(), undefined, options)
);
} finally {
this._busy--;
if (!this.verified) {
const choice = await new ConfirmDialog({
title: nls.localize('theia/vsx-registry/confirmDialogTitle', 'Are you sure you want to proceed with the installation ?'),
msg: nls.localize('theia/vsx-registry/confirmDialogMessage', 'The extension "{0}" is unverified and might pose a security risk.', this.displayName)
}).open();
if (choice) {
this.doInstall(options);
}
} else {
this.doInstall(options);
}
}

Expand All @@ -322,6 +331,17 @@ export class VSXExtension implements VSXExtensionData, TreeElement {
}
}

protected async doInstall(options?: PluginDeployOptions): Promise<void> {
this._busy++;
try {
await this.progressService.withProgress(nls.localizeByDefault("Installing extension '{0}' v{1}...", this.id, this.version ?? 0), 'extensions', () =>
this.pluginServer.deploy(this.uri.toString(), undefined, options)
);
} finally {
this._busy--;
}
}

handleContextMenu(e: React.MouseEvent<HTMLElement, MouseEvent>): void {
e.preventDefault();
this.contextMenuRenderer.render({
Expand Down Expand Up @@ -464,7 +484,7 @@ export namespace VSXExtensionComponent {

export class VSXExtensionComponent<Props extends VSXExtensionComponent.Props = VSXExtensionComponent.Props> extends AbstractVSXExtensionComponent<Props> {
override render(): React.ReactNode {
const { iconUrl, publisher, displayName, description, version, downloadCount, averageRating, tooltip } = this.props.extension;
const { iconUrl, publisher, displayName, description, version, downloadCount, averageRating, tooltip, verified } = this.props.extension;

return <div
className='theia-vsx-extension noselect'
Expand All @@ -491,7 +511,16 @@ export class VSXExtensionComponent<Props extends VSXExtensionComponent.Props = V
</div>
<div className='noWrapInfo theia-vsx-extension-description'>{description}</div>
<div className='theia-vsx-extension-action-bar'>
<span className='noWrapInfo theia-vsx-extension-publisher'>{publisher}</span>
<div className='theia-vsx-extension-publisher-container'>
{verified === true ? (
<i className={codicon('verified-filled')} />
) : verified === false ? (
<i className={codicon('verified')} />
) : (
<i className={codicon('question')} />
)}
<span className='noWrapInfo theia-vsx-extension-publisher'>{publisher}</span>
</div>
{this.renderAction(this.props.host)}
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,12 @@ export class VSXExtensionsContribution extends AbstractViewContribution<VSXExten
hcLight: Color.black
}, description: 'Border color for a table row of the extension editor view'
},
{
id: 'extensionIcon.verifiedForeground', defaults: {
dark: '#40a6ff',
light: '#40a6ff'
}, description: 'The icon color for extension verified publisher.'
},
);
}

Expand Down
75 changes: 61 additions & 14 deletions packages/vsx-registry/src/browser/vsx-extensions-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import { PreferenceInspectionScope, PreferenceService } from '@theia/core/lib/br
import { WorkspaceService } from '@theia/workspace/lib/browser';
import { RecommendedExtensions } from './recommended-extensions/recommended-extensions-preference-contribution';
import URI from '@theia/core/lib/common/uri';
import { VSXExtensionRaw, VSXResponseError, VSXSearchOptions } from '@theia/ovsx-client/lib/ovsx-types';
import { OVSXClient, VSXAllVersions, VSXExtensionRaw, VSXResponseError, VSXSearchEntry, VSXSearchOptions } from '@theia/ovsx-client/lib/ovsx-types';
import { OVSXClientProvider } from '../common/ovsx-client-provider';
import { RequestContext, RequestService } from '@theia/core/shared/@theia/request';
import { OVSXApiFilter } from '@theia/ovsx-client';
Expand Down Expand Up @@ -113,6 +113,13 @@ export class VSXExtensionsModel {
return this._recommended.values();
}

setOnlyShowVerifiedExtensions(bool: boolean): void {
if (this.preferences.get('extensions.onlyShowVerifiedExtensions') !== bool) {
this.preferences.updateValue('extensions.onlyShowVerifiedExtensions', bool);
}
this.updateSearchResult();
}

isInstalled(id: string): boolean {
return this._installed.has(id);
}
Expand Down Expand Up @@ -208,9 +215,8 @@ export class VSXExtensionsModel {

protected doUpdateSearchResult(param: VSXSearchOptions, token: CancellationToken): Promise<void> {
return this.doChange(async () => {
const searchResult = new Set<string>();
this._searchResult = new Set<string>();
if (!param.query) {
this._searchResult = searchResult;
return;
}
const client = await this.clientProvider();
Expand All @@ -225,20 +231,55 @@ export class VSXExtensionsModel {
if (!allVersions) {
continue;
}
this.setExtension(id).update(Object.assign(data, {
publisher: data.namespace,
downloadUrl: data.files.download,
iconUrl: data.files.icon,
readmeUrl: data.files.readme,
licenseUrl: data.files.license,
version: allVersions.version
}));
searchResult.add(id);
if (this.preferences.get('extensions.onlyShowVerifiedExtensions')) {
this.fetchVerifiedStatus(id, client, allVersions).then(verified => {
this.doChange(() => {
this.addExtensions(data, id, allVersions, !!verified);
return Promise.resolve();
});
});
} else {
this.addExtensions(data, id, allVersions);
this.fetchVerifiedStatus(id, client, allVersions).then(verified => {
this.doChange(() => {
let extension = this.getExtension(id);
extension = this.setExtension(id);
extension.update(Object.assign({
verified: verified
}));
return Promise.resolve();
});
});
}
}
this._searchResult = searchResult;
}, token);
}

protected async fetchVerifiedStatus(id: string, client: OVSXClient, allVersions: VSXAllVersions): Promise<boolean | undefined> {
const res = await client.query({ extensionId: id, extensionVersion: allVersions.version, includeAllVersions: true });
let verified = res.extensions?.[0].verified;
if (!verified && res.extensions?.[0].publishedBy.loginName === 'open-vsx') {
verified = true;
}
return verified;
}

protected addExtensions(data: VSXSearchEntry, id: string, allVersions: VSXAllVersions, verified?: boolean): void {
if (!this.preferences.get('extensions.onlyShowVerifiedExtensions') || verified) {
const extension = this.setExtension(id);
extension.update(Object.assign(data, {
publisher: data.namespace,
downloadUrl: data.files.download,
iconUrl: data.files.icon,
readmeUrl: data.files.readme,
licenseUrl: data.files.license,
version: allVersions.version,
verified: verified
}));
this._searchResult.add(id);
}
}

protected async updateInstalled(): Promise<void> {
const prevInstalled = this._installed;
return this.doChange(async () => {
Expand Down Expand Up @@ -331,14 +372,20 @@ export class VSXExtensionsModel {
if (data.error) {
return this.onDidFailRefresh(id, data.error);
}
if (!data.verified) {
if (data.publishedBy.loginName === 'open-vsx') {
data.verified = true;
}
}
extension = this.setExtension(id);
extension.update(Object.assign(data, {
publisher: data.namespace,
downloadUrl: data.files.download,
iconUrl: data.files.icon,
readmeUrl: data.files.readme,
licenseUrl: data.files.license,
version: data.version
version: data.version,
verified: data.verified
}));
return extension;
} catch (e) {
Expand Down
58 changes: 58 additions & 0 deletions packages/vsx-registry/src/browser/vsx-extensions-preferences.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// *****************************************************************************
// Copyright (C) 2023 Ericsson and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************

import { interfaces } from '@theia/core/shared/inversify';
import {
createPreferenceProxy,
PreferenceProxy,
PreferenceService,
PreferenceSchema,
PreferenceContribution
} from '@theia/core/lib/browser/preferences';
import { nls } from '@theia/core';

export const VsxExtensionsPreferenceSchema: PreferenceSchema = {
'type': 'object',
properties: {
'extensions.onlyShowVerifiedExtensions': {
type: 'boolean',
default: false,
description: nls.localize('theia/vsx-registry/onlyShowVerifiedExtensionsDescription', 'This allows the {0} to only show verified extensions.', 'Open VSX Registry')
},
}
};

export interface VsxExtensionsConfiguration {
'extensions.onlyShowVerifiedExtensions': boolean;
}

export const VsxExtensionsPreferenceContribution = Symbol('VsxExtensionsPreferenceContribution');
export const VsxExtensionsPreferences = Symbol('VsxExtensionsPreferences');
export type VsxExtensionsPreferences = PreferenceProxy<VsxExtensionsConfiguration>;

export function createVsxExtensionsPreferences(preferences: PreferenceService, schema: PreferenceSchema = VsxExtensionsPreferenceSchema): VsxExtensionsPreferences {
return createPreferenceProxy(preferences, schema);
}

export function bindVsxExtensionsPreferences(bind: interfaces.Bind): void {
bind(VsxExtensionsPreferences).toDynamicValue(ctx => {
const preferences = ctx.container.get<PreferenceService>(PreferenceService);
const contribution = ctx.container.get<PreferenceContribution>(VsxExtensionsPreferenceContribution);
return createVsxExtensionsPreferences(preferences, contribution.schema);
}).inSingletonScope();
bind(VsxExtensionsPreferenceContribution).toConstantValue({ schema: VsxExtensionsPreferenceSchema });
bind(PreferenceContribution).toService(VsxExtensionsPreferenceContribution);
}
Loading

0 comments on commit abc49cd

Please sign in to comment.