Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -472,7 +472,7 @@ private String tryGetAssetPath(String type) {
return null;
}

protected TempFile getIcon(ExtensionVersion extVersion) throws IOException {
public TempFile getIcon(ExtensionVersion extVersion) throws IOException {
var iconPath = tryGetAssetPath(ExtensionQueryResult.ExtensionFile.FILE_ICON);
if (StringUtils.isEmpty(iconPath)) {
loadPackageJson();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,32 @@
* ****************************************************************************** */
package org.eclipse.openvsx.publish;

import com.google.common.base.Joiner;
import jakarta.persistence.EntityManager;
import jakarta.transaction.Transactional;
import java.io.IOException;
import java.time.LocalDateTime;
import java.util.List;
import java.util.function.Consumer;

import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.openvsx.ExtensionProcessor;
import org.eclipse.openvsx.ExtensionService;
import org.eclipse.openvsx.ExtensionValidator;
import org.eclipse.openvsx.UserService;
import org.eclipse.openvsx.adapter.VSCodeIdNewExtensionJobRequest;
import org.eclipse.openvsx.entities.*;
import org.eclipse.openvsx.entities.Extension;
import org.eclipse.openvsx.entities.ExtensionScan;
import org.eclipse.openvsx.entities.ExtensionVersion;
import org.eclipse.openvsx.entities.FileResource;
import org.eclipse.openvsx.entities.PersonalAccessToken;
import org.eclipse.openvsx.entities.UserData;
import org.eclipse.openvsx.extension_control.ExtensionControlService;
import org.eclipse.openvsx.repositories.RepositoryService;
import org.eclipse.openvsx.scanning.ExtensionScanService;
import org.eclipse.openvsx.util.*;
import org.eclipse.openvsx.util.ErrorResultException;
import org.eclipse.openvsx.util.ExtensionId;
import org.eclipse.openvsx.util.NamingUtil;
import org.eclipse.openvsx.util.TempFile;
import org.jobrunr.scheduling.JobRequestScheduler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand All @@ -33,10 +44,10 @@
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerErrorException;

import java.io.IOException;
import java.time.LocalDateTime;
import java.util.List;
import java.util.function.Consumer;
import com.google.common.base.Joiner;

import jakarta.persistence.EntityManager;
import jakarta.transaction.Transactional;

@Component
public class PublishExtensionVersionHandler {
Expand Down Expand Up @@ -134,6 +145,8 @@ private ExtensionVersion createExtensionVersion(ExtensionProcessor processor, Us
extVersion.setPublishedWith(token);
extVersion.setActive(false);

validateIcon(processor, extVersion);

var extension = repositories.findExtension(extensionName, namespace);
if (extension == null) {
extension = new Extension();
Expand Down Expand Up @@ -202,6 +215,17 @@ private void checkLicense(ExtensionVersion extVersion, TempFile licenseFile) {
}
}

private void validateIcon(ExtensionProcessor processor, ExtensionVersion extVersion) {
try (var iconTmpFile = processor.getIcon(extVersion)) {
var ext = FilenameUtils.getExtension(iconTmpFile.getPath().toString());
if ("svg".equalsIgnoreCase(ext)) {
throw new ErrorResultException("This extension cannot be accepted as it contains a denied icon image format.");
}
} catch (IOException e) {
logger.warn("Failed to check whether icon is a denied format or not", e);
}
}

private void validateMetadata(ExtensionVersion extVersion) {
var metadataIssues = validator.validateMetadata(extVersion);
if (!metadataIssues.isEmpty()) {
Expand Down
12 changes: 8 additions & 4 deletions server/src/test/java/org/eclipse/openvsx/RegistryAPITest.java
Original file line number Diff line number Diff line change
Expand Up @@ -1709,7 +1709,7 @@ void testPublishSameVersionDifferentTargetPlatformPreRelease() throws Exception
return extensionVersion.getVersion().equals(extVersion.getVersion());
});

var bytes = createExtensionPackage("bar", "1.0.0", null, true, TargetPlatform.NAME_LINUX_X64);
var bytes = createExtensionPackage("bar", "1.0.0", null, true, TargetPlatform.NAME_LINUX_X64, "/tmp/known-good-icon.jpg");
mockMvc.perform(post("/api/-/publish?token={token}", "my_token")
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.content(bytes))
Expand All @@ -1731,7 +1731,7 @@ void testPublishSameVersionDifferentTargetPlatformStableRelease() throws Excepti
return extensionVersion.getVersion().equals(extVersion.getVersion());
});

var bytes = createExtensionPackage("bar", "1.5.0", null, false, TargetPlatform.NAME_ALPINE_ARM64);
var bytes = createExtensionPackage("bar", "1.5.0", null, false, TargetPlatform.NAME_ALPINE_ARM64,"/tmp/known-good-icon.png");
mockMvc.perform(post("/api/-/publish?token={token}", "my_token")
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.content(bytes))
Expand Down Expand Up @@ -2483,10 +2483,10 @@ private String warningJson(String message) throws JsonProcessingException {
}

private byte[] createExtensionPackage(String name, String version, String license) throws IOException {
return createExtensionPackage(name, version, license, false, null);
return createExtensionPackage(name, version, license, false, null, "/tmp/known-good-icon.png");
}

private byte[] createExtensionPackage(String name, String version, String license, boolean preRelease, String targetPlatform) throws IOException {
private byte[] createExtensionPackage(String name, String version, String license, boolean preRelease, String targetPlatform, String iconPath) throws IOException {
var bytes = new ByteArrayOutputStream();
var archive = new ZipOutputStream(bytes);
archive.putNextEntry(new ZipEntry("extension.vsixmanifest"));
Expand Down Expand Up @@ -2516,6 +2516,7 @@ private byte[] createExtensionPackage(String name, String version, String licens
"<Dependencies/>" +
"<Assets>" +
"<Asset Type=\"Microsoft.VisualStudio.Code.Manifest\" Path=\"extension/package.json\" Addressable=\"true\" />" +
"<Asset Type=\"Microsoft.VisualStudio.Services.Icons.Default\" Path=\"" + iconPath + "\" Addressable=\"true\" />" +
"</Assets>" +
"</PackageManifest>";
archive.write(vsixmanifest.getBytes());
Expand All @@ -2529,6 +2530,9 @@ private byte[] createExtensionPackage(String name, String version, String licens
"}";
archive.write(packageJson.getBytes());
archive.closeEntry();
archive.putNextEntry(new ZipEntry(iconPath));
archive.write("placeholder".getBytes());
archive.closeEntry();
archive.finish();
return bytes.toByteArray();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/********************************************************************************
/** ******************************************************************************
* Copyright (c) 2025 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
Expand All @@ -9,10 +9,20 @@
* http://www.eclipse.org/legal/epl-2.0.
*
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
******************************************************************************* */
package org.eclipse.openvsx.publish;

import jakarta.persistence.EntityManager;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import java.io.IOException;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.List;
import java.util.Optional;

import org.eclipse.openvsx.ExtensionProcessor;
import org.eclipse.openvsx.ExtensionValidator;
import org.eclipse.openvsx.UserService;
Expand All @@ -24,23 +34,17 @@
import org.eclipse.openvsx.repositories.RepositoryService;
import org.eclipse.openvsx.scanning.ExtensionScanService;
import org.eclipse.openvsx.util.ErrorResultException;
import org.eclipse.openvsx.util.TempFile;
import org.jobrunr.scheduling.JobRequestScheduler;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.ArgumentMatchers;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.time.LocalDateTime;
import java.util.Collections;
import java.util.List;
import java.util.Optional;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import jakarta.persistence.EntityManager;

@ExtendWith(MockitoExtension.class)
class PublishExtensionVersionHandlerTest {
Expand Down Expand Up @@ -87,20 +91,21 @@ void setUp() throws Exception {
extensionControl,
scanService
);

// Lenient: not all tests need this mock
org.mockito.Mockito.lenient()
.when(extensionControl.getMaliciousExtensionIds())
.thenReturn(Collections.emptyList());
.when(extensionControl.getMaliciousExtensionIds())
.thenReturn(Collections.emptyList());
}

@Test
void shouldCreateExtensionWhenNamespaceExists() {
void shouldCreateExtensionWhenNamespaceExists() throws IOException {
// Happy path: extension version gets persisted.
var processor = org.mockito.Mockito.mock(ExtensionProcessor.class);
when(processor.getNamespace()).thenReturn("publisher");
when(processor.getExtensionName()).thenReturn("demo");
when(processor.getVersion()).thenReturn("2.0.0");
when(processor.getIcon(ArgumentMatchers.any())).thenReturn(new TempFile("sample-icon-image", ".png"));
when(processor.getExtensionDependencies()).thenReturn(List.of());
when(processor.getBundledExtensions()).thenReturn(List.of());

Expand Down Expand Up @@ -151,10 +156,39 @@ void shouldFailWhenNamespaceDoesNotExist() {
.hasMessageContaining("Unknown publisher");
}

@Test
void shouldFailWhenImageFormatIsDisallowed() throws IOException {

var processor = org.mockito.Mockito.mock(ExtensionProcessor.class);
when(processor.getNamespace()).thenReturn("publisher");
when(processor.getExtensionName()).thenReturn("demo");
when(processor.getVersion()).thenReturn("2.0.0");
when(processor.getIcon(ArgumentMatchers.any())).thenReturn(new TempFile("sample-icon-image", ".svg"));

var metadata = new ExtensionVersion();
metadata.setDisplayName("Demo OK");
metadata.setVersion("2.0.0");
metadata.setTargetPlatform("any");
when(processor.getMetadata()).thenReturn(metadata);

var namespace = buildNamespace("publisher");
var user = new org.eclipse.openvsx.entities.UserData();
var token = new PersonalAccessToken();
token.setUser(user);

when(repositories.findNamespace("publisher")).thenReturn(namespace);
when(users.hasPublishPermission(user, namespace)).thenReturn(true);
when(validator.validateExtensionVersion("2.0.0")).thenReturn(Optional.empty());
when(validator.validateExtensionName("demo")).thenReturn(Optional.empty());

assertThatThrownBy(() -> handler.createExtensionVersion(processor, token, LocalDateTime.now(), false))
.isInstanceOf(ErrorResultException.class)
.hasMessageContaining("contains a denied icon image format");
}

private Namespace buildNamespace(String name) {
var namespace = new Namespace();
namespace.setName(name);
return namespace;
}
}

Loading