Skip to content

Commit 6880fa4

Browse files
author
amvanbaren
committed
Web extension resources return incorrect MIME type
Use [Content_Types].xml to set FileResource contentType Add test case for default content type: application/octet-stream Add migration to set FileResource.contentType Set contentType for RESOURCE type Remove dot ('.') if extension starts with a dot Use utf-8 charset for text resources.
1 parent af09fea commit 6880fa4

File tree

99 files changed

+2632
-627
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

99 files changed

+2632
-627
lines changed

server/src/main/java/org/eclipse/openvsx/ExtensionProcessor.java

Lines changed: 58 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,22 @@
1515
import com.fasterxml.jackson.databind.node.MissingNode;
1616
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
1717
import org.apache.commons.codec.digest.DigestUtils;
18+
import org.apache.commons.io.FilenameUtils;
1819
import org.apache.commons.lang3.StringUtils;
20+
1921
import org.eclipse.openvsx.adapter.ExtensionQueryResult;
2022
import org.eclipse.openvsx.entities.ExtensionVersion;
2123
import org.eclipse.openvsx.entities.FileResource;
2224
import org.eclipse.openvsx.util.*;
2325
import org.slf4j.Logger;
2426
import org.slf4j.LoggerFactory;
2527
import org.springframework.data.util.Pair;
28+
import org.springframework.http.MediaType;
29+
import org.xml.sax.SAXException;
2630

31+
import javax.xml.parsers.DocumentBuilderFactory;
32+
import javax.xml.parsers.ParserConfigurationException;
33+
import java.io.ByteArrayInputStream;
2734
import java.io.EOFException;
2835
import java.io.IOException;
2936
import java.nio.charset.StandardCharsets;
@@ -295,17 +302,22 @@ private List<String> getEngines(JsonNode node) {
295302
}
296303

297304
public List<FileResource> getFileResources(ExtensionVersion extVersion) {
298-
var resources = new ArrayList<FileResource>();
305+
readInputStream();
306+
var contentTypes = loadContentTypes();
299307
var mappers = List.<Function<ExtensionVersion, FileResource>>of(
300308
this::getManifest, this::getReadme, this::getChangelog, this::getLicense, this::getIcon, this::getVsixManifest
301309
);
302310

303-
mappers.forEach(mapper -> Optional.of(extVersion).map(mapper).ifPresent(resources::add));
304-
return resources;
311+
return mappers.stream()
312+
.map(mapper -> mapper.apply(extVersion))
313+
.filter(Objects::nonNull)
314+
.map(resource -> setContentType(resource, contentTypes))
315+
.collect(Collectors.toList());
305316
}
306317

307318
public void processEachResource(ExtensionVersion extVersion, Consumer<FileResource> processor) {
308319
readInputStream();
320+
var contentTypes = loadContentTypes();
309321
zipFile.stream()
310322
.filter(zipEntry -> !zipEntry.isDirectory())
311323
.map(zipEntry -> {
@@ -324,6 +336,7 @@ public void processEachResource(ExtensionVersion extVersion, Consumer<FileResour
324336
resource.setName(zipEntry.getName());
325337
resource.setType(FileResource.RESOURCE);
326338
resource.setContent(bytes);
339+
setContentType(resource, contentTypes);
327340
return resource;
328341
})
329342
.filter(Objects::nonNull)
@@ -360,6 +373,7 @@ public FileResource generateSha256Checksum(ExtensionVersion extVersion) {
360373
sha256.setName(NamingUtil.toFileFormat(extVersion, ".sha256"));
361374
sha256.setType(FileResource.DOWNLOAD_SHA256);
362375
sha256.setContent(hash.getBytes(StandardCharsets.UTF_8));
376+
sha256.setContentType(MediaType.TEXT_PLAIN_VALUE);
363377
return sha256;
364378
}
365379

@@ -418,9 +432,7 @@ public FileResource getLicense(ExtensionVersion extVersion) {
418432
var fileName = matcher.group("file");
419433
var bytes = ArchiveUtil.readEntry(zipFile, "extension/" + fileName);
420434
if (bytes != null) {
421-
var lastSegmentIndex = fileName.lastIndexOf('/');
422-
var lastSegment = fileName.substring(lastSegmentIndex + 1);
423-
license.setName(lastSegment);
435+
license.setName(FilenameUtils.getName(fileName));
424436
license.setContent(bytes);
425437
detectLicense(bytes, extVersion);
426438
return license;
@@ -439,6 +451,44 @@ public FileResource getLicense(ExtensionVersion extVersion) {
439451
return license;
440452
}
441453

454+
private Map<String, String> loadContentTypes() {
455+
var bytes = ArchiveUtil.readEntry(zipFile, "[Content_Types].xml");
456+
var contentTypes = parseContentTypesXml(bytes);
457+
contentTypes.putIfAbsent("vsix", "application/zip");
458+
return contentTypes;
459+
}
460+
461+
private Map<String, String> parseContentTypesXml(byte[] content) {
462+
var contentTypes = new HashMap<String, String>();
463+
try (var input = new ByteArrayInputStream(content)) {
464+
var document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(input);
465+
var elements = document.getDocumentElement().getElementsByTagName("Default");
466+
for(var i = 0; i < elements.getLength(); i++) {
467+
var element = elements.item(i);
468+
var attributes = element.getAttributes();
469+
var extension = attributes.getNamedItem("Extension").getTextContent();
470+
if(extension.startsWith(".")) {
471+
extension = extension.substring(1);
472+
}
473+
474+
var contentType = attributes.getNamedItem("ContentType").getTextContent();
475+
contentTypes.put(extension, contentType);
476+
}
477+
} catch (IOException | ParserConfigurationException | SAXException e) {
478+
logger.error("failed to read content types", e);
479+
contentTypes.clear();
480+
}
481+
482+
return contentTypes;
483+
}
484+
485+
private FileResource setContentType(FileResource resource, Map<String, String> contentTypes) {
486+
var fileExtension = FilenameUtils.getExtension(resource.getName());
487+
var contentType = contentTypes.getOrDefault(fileExtension, MediaType.APPLICATION_OCTET_STREAM_VALUE);
488+
resource.setContentType(contentType);
489+
return resource;
490+
}
491+
442492
private void detectLicense(byte[] content, ExtensionVersion extVersion) {
443493
if (StringUtils.isEmpty(extVersion.getLicense())) {
444494
var detection = new LicenseDetection();
@@ -464,9 +514,7 @@ private Pair<byte[], String> readFromAlternateNames(String[] names) {
464514
var entry = ArchiveUtil.getEntryIgnoreCase(zipFile, name);
465515
if (entry != null) {
466516
var bytes = ArchiveUtil.readEntry(zipFile, entry);
467-
var lastSegmentIndex = entry.getName().lastIndexOf('/');
468-
var lastSegment = entry.getName().substring(lastSegmentIndex + 1);
469-
return Pair.of(bytes, lastSegment);
517+
return Pair.of(bytes, FilenameUtils.getName(entry.getName()));
470518
}
471519
}
472520
return null;
@@ -506,12 +554,7 @@ protected FileResource getIcon(ExtensionVersion extVersion) {
506554

507555
var icon = new FileResource();
508556
icon.setExtension(extVersion);
509-
var fileNameIndex = iconPath.lastIndexOf('/');
510-
var iconName = fileNameIndex >= 0
511-
? iconPath.substring(fileNameIndex + 1)
512-
: iconPath;
513-
514-
icon.setName(iconName);
557+
icon.setName(FilenameUtils.getName(iconPath));
515558
icon.setType(FileResource.ICON);
516559
icon.setContent(bytes);
517560
return icon;

server/src/main/java/org/eclipse/openvsx/ExtensionValidator.java

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,13 @@
1010
package org.eclipse.openvsx;
1111

1212
import org.apache.commons.lang3.StringUtils;
13-
import org.apache.tika.Tika;
14-
import org.apache.tika.mime.MediaType;
15-
import org.apache.tika.mime.MimeTypeException;
16-
import org.apache.tika.mime.MimeTypes;
1713
import org.eclipse.openvsx.entities.ExtensionVersion;
1814
import org.eclipse.openvsx.entities.SemanticVersion;
1915
import org.eclipse.openvsx.json.NamespaceDetailsJson;
2016
import org.eclipse.openvsx.util.TargetPlatform;
2117
import org.eclipse.openvsx.util.VersionAlias;
2218
import org.springframework.stereotype.Component;
2319

24-
import java.io.ByteArrayInputStream;
25-
import java.io.IOException;
2620
import java.net.MalformedURLException;
2721
import java.net.URL;
2822
import java.util.ArrayList;
@@ -82,22 +76,6 @@ public List<Issue> validateNamespaceDetails(NamespaceDetailsJson json) {
8276
issues.add(new Issue("Invalid Twitter URL"));
8377
}
8478

85-
if(json.logoBytes != null) {
86-
try (var in = new ByteArrayInputStream(json.logoBytes)) {
87-
var tika = new Tika();
88-
var detectedType = tika.detect(in, json.logo);
89-
var logoType = MimeTypes.getDefaultMimeTypes().getRegisteredMimeType(detectedType);
90-
if(logoType != null) {
91-
json.logo = "logo-" + json.name + "-" + System.currentTimeMillis() + logoType.getExtension();
92-
if(!logoType.getType().equals(MediaType.image("png")) && !logoType.getType().equals(MediaType.image("jpg"))) {
93-
issues.add(new Issue("Namespace logo should be of png or jpg type"));
94-
}
95-
}
96-
} catch (IOException | MimeTypeException e) {
97-
issues.add(new Issue("Failed to read namespace logo"));
98-
}
99-
}
100-
10179
return issues;
10280
}
10381

server/src/main/java/org/eclipse/openvsx/LocalRegistryService.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
package org.eclipse.openvsx;
1111

1212
import com.google.common.collect.Maps;
13+
import jakarta.persistence.EntityManager;
14+
import jakarta.transaction.Transactional;
1315
import org.apache.commons.lang3.StringUtils;
1416
import org.eclipse.openvsx.cache.CacheService;
1517
import org.eclipse.openvsx.eclipse.EclipseService;
@@ -28,16 +30,13 @@
2830
import org.springframework.cache.annotation.Cacheable;
2931
import org.springframework.dao.DataIntegrityViolationException;
3032
import org.springframework.data.domain.PageRequest;
31-
import org.springframework.data.elasticsearch.core.SearchHit;
3233
import org.springframework.data.elasticsearch.core.SearchHits;
3334
import org.springframework.http.HttpStatus;
3435
import org.springframework.http.ResponseEntity;
3536
import org.springframework.retry.annotation.Retryable;
3637
import org.springframework.stereotype.Component;
3738
import org.springframework.web.server.ResponseStatusException;
3839

39-
import jakarta.persistence.EntityManager;
40-
import jakarta.transaction.Transactional;
4140
import java.io.InputStream;
4241
import java.util.*;
4342
import java.util.stream.Collectors;

server/src/main/java/org/eclipse/openvsx/UserService.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111

1212
import com.google.common.base.Joiner;
1313
import org.apache.commons.io.IOUtils;
14+
import org.apache.tika.Tika;
15+
import org.apache.tika.mime.MediaType;
16+
import org.apache.tika.mime.MimeTypeException;
17+
import org.apache.tika.mime.MimeTypes;
1418
import org.eclipse.openvsx.cache.CacheService;
1519
import org.eclipse.openvsx.entities.*;
1620
import org.eclipse.openvsx.json.AccessTokenJson;
@@ -221,6 +225,23 @@ public ResultJson updateNamespaceDetails(NamespaceDetailsJson details) {
221225
throw new ErrorResultException(message);
222226
}
223227

228+
String contentType = null;
229+
if(details.logoBytes != null) {
230+
try (var in = new ByteArrayInputStream(details.logoBytes)) {
231+
var tika = new Tika();
232+
contentType = tika.detect(in, details.logo);
233+
var logoType = MimeTypes.getDefaultMimeTypes().getRegisteredMimeType(contentType);
234+
if(logoType != null) {
235+
details.logo = "logo-" + details.name + "-" + System.currentTimeMillis() + logoType.getExtension();
236+
if(!logoType.getType().equals(MediaType.image("png")) && !logoType.getType().equals(MediaType.image("jpg"))) {
237+
throw new ErrorResultException("Namespace logo should be of png or jpg type");
238+
}
239+
}
240+
} catch (IOException | MimeTypeException e) {
241+
throw new ErrorResultException("Failed to read namespace logo");
242+
}
243+
}
244+
224245
if(!Objects.equals(details.displayName, namespace.getDisplayName())) {
225246
namespace.setDisplayName(details.displayName);
226247
}
@@ -259,11 +280,13 @@ public ResultJson updateNamespaceDetails(NamespaceDetailsJson details) {
259280

260281
namespace.setLogoName(details.logo);
261282
namespace.setLogoBytes(details.logoBytes);
283+
namespace.setLogoContentType(contentType);
262284
storeNamespaceLogo(namespace);
263285
} else if (namespace.getLogoStorageType() != null) {
264286
storageUtil.removeNamespaceLogo(namespace);
265287
namespace.setLogoName(null);
266288
namespace.setLogoBytes(null);
289+
namespace.setLogoContentType(null);
267290
namespace.setLogoStorageType(null);
268291
}
269292
}

server/src/main/java/org/eclipse/openvsx/adapter/LocalVSCodeService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -423,7 +423,7 @@ private ResponseEntity<byte[]> browseFile(
423423
String version
424424
) {
425425
if (resource.getStorageType().equals(FileResource.STORAGE_DB)) {
426-
var headers = storageUtil.getFileResponseHeaders(resource.getName());
426+
var headers = storageUtil.getFileResponseHeaders(resource);
427427
return new ResponseEntity<>(resource.getContent(), headers, HttpStatus.OK);
428428
} else {
429429
var namespace = new Namespace();

server/src/main/java/org/eclipse/openvsx/entities/FileResource.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ public class FileResource {
4747
@Basic(fetch = FetchType.LAZY)
4848
byte[] content;
4949

50+
String contentType;
51+
5052
@Column(length = 32)
5153
String storageType;
5254

@@ -90,6 +92,14 @@ public void setContent(byte[] content) {
9092
this.content = content;
9193
}
9294

95+
public String getContentType() {
96+
return contentType;
97+
}
98+
99+
public void setContentType(String contentType) {
100+
this.contentType = contentType;
101+
}
102+
93103
public String getStorageType() {
94104
return storageType;
95105
}

server/src/main/java/org/eclipse/openvsx/entities/Namespace.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ public class Namespace implements Serializable {
5757
@Column(length = 32)
5858
String logoStorageType;
5959

60+
String logoContentType;
61+
6062
@ElementCollection
6163
@MapKeyColumn(name = "provider")
6264
@Column(name = "social_link")
@@ -133,6 +135,14 @@ public void setLogoStorageType(String logoStorageType) {
133135
this.logoStorageType = logoStorageType;
134136
}
135137

138+
public String getLogoContentType() {
139+
return logoContentType;
140+
}
141+
142+
public void setLogoContentType(String logoContentType) {
143+
this.logoContentType = logoContentType;
144+
}
145+
136146
public String getWebsite() {
137147
return website;
138148
}
@@ -206,6 +216,7 @@ public boolean equals(Object o) {
206216
&& Objects.equals(logoName, namespace.logoName)
207217
&& Arrays.equals(logoBytes, namespace.logoBytes)
208218
&& Objects.equals(logoStorageType, namespace.logoStorageType)
219+
&& Objects.equals(logoContentType, namespace.logoContentType)
209220
&& Objects.equals(socialLinks, namespace.socialLinks)
210221
&& Objects.equals(extensions, namespace.extensions)
211222
&& Objects.equals(memberships, namespace.memberships);
@@ -214,7 +225,7 @@ public boolean equals(Object o) {
214225
@Override
215226
public int hashCode() {
216227
int result = Objects.hash(id, publicId, name, displayName, description, website, supportLink, logoName,
217-
logoStorageType, socialLinks, extensions, memberships);
228+
logoStorageType, logoContentType, socialLinks, extensions, memberships);
218229
result = 31 * result + Arrays.hashCode(logoBytes);
219230
return result;
220231
}

server/src/main/java/org/eclipse/openvsx/migration/MigrationRunner.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ public void run(HandlerJobRequest<?> jobRequest) throws Exception {
4646
fixTargetPlatformMigration();
4747
generateSha256ChecksumMigration();
4848
extensionVersionSignatureMigration();
49+
setFileResourceContentTypeMigration();
50+
setNamespaceLogoContentTypeMigration();
4951
}
5052

5153
private void extractResourcesMigration() {
@@ -89,4 +91,16 @@ private void extensionVersionSignatureMigration() {
8991
scheduler.enqueue(new HandlerJobRequest<>(GenerateKeyPairJobRequestHandler.class));
9092
}
9193
}
94+
95+
private void setFileResourceContentTypeMigration() {
96+
var jobName = "SetFileResourceContentTypeMigration";
97+
var handler = SetFileResourceContentTypeJobRequestHandler.class;
98+
repositories.findNotMigratedFileResourceContentTypes().forEach(item -> migrations.enqueueMigration(jobName, handler, item));
99+
}
100+
101+
private void setNamespaceLogoContentTypeMigration() {
102+
var jobName = "SetNamespaceLogoContentTypeMigration";
103+
var handler = SetNamespaceLogoContentTypeJobRequestHandler.class;
104+
repositories.findNotMigratedNamespaceLogoContentTypes().forEach(item -> migrations.enqueueMigration(jobName, handler, item));
105+
}
92106
}

server/src/main/java/org/eclipse/openvsx/migration/MigrationService.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
* ****************************************************************************** */
1010
package org.eclipse.openvsx.migration;
1111

12+
import jakarta.persistence.EntityManager;
13+
import jakarta.transaction.Transactional;
1214
import org.eclipse.openvsx.entities.ExtensionVersion;
1315
import org.eclipse.openvsx.entities.FileResource;
1416
import org.eclipse.openvsx.entities.MigrationItem;
@@ -25,8 +27,6 @@
2527
import org.springframework.stereotype.Component;
2628
import org.springframework.web.client.RestTemplate;
2729

28-
import jakarta.persistence.EntityManager;
29-
import jakarta.transaction.Transactional;
3030
import java.io.IOException;
3131
import java.nio.charset.StandardCharsets;
3232
import java.nio.file.Files;

0 commit comments

Comments
 (0)