Skip to content

Commit b4655f4

Browse files
authored
feat(IDE-1652): migrate HTML settings page onto FolderConfigSettings + ExecuteCommandBridge (3/3) (#396)
* feat(IDE-1652): introduce LsSettingsRegistry, protocol message objects, and ACTIVATE_SNYK_SECRETS constant Adds the foundational types needed for the upcoming LS settings rewrite: - LsKey enum: typed keys for all LS settings fields - LsFolderSettingsKeys: constants for folder-scoped LS config fields - LsSettingsRegistry: single source of truth mapping LsKey entries to Preferences keys, defaults, and change-tracking metadata - ConfigSetting: wire-format value object for a single LS setting - LspFolderConfig: wire-format value object for per-folder LS config - LspConfigurationParam: top-level outbound LS config payload - Preferences.ACTIVATE_SNYK_SECRETS: constant required by LsSettingsRegistry - LsBinaries: minor update All new classes are unreferenced by existing code — safe to land standalone. Tests cover each value object and serialisation round-trips. * feat(IDE-1652): migrate HTML settings page onto FolderConfigSettings and ExecuteCommandBridge (3/3) - HTMLSettingsPreferencePage: rewired to read/write settings via FolderConfigSettings and LsSettingsRegistry instead of the legacy Settings / IdeConfigData path; bridges auth and settings updates through ExecuteCommandBridge - settings-fallback.html: sync from snyk-ls shared_ide_resources with updated JS wiring for new settings bridge commands - HTMLSettingsPreferencePageTest: updated to cover new wiring After this PR all legacy POJOs (Settings, FolderConfig, FolderConfigs, FolderConfigsParam, IdeConfigData) and native UI shells (NativeProjectPropertyPage, PreferencesPage, TokenFieldEditor) are dead code with no callers. PR #393 deletes them. * fix: guard scan_command_config convertValue against IllegalArgumentException Non-object nodes and null boolean fields throw IllegalArgumentException which the outer JsonProcessingException catch misses, silently aborting the whole save. Guard with isObject() check and isolate the failure to the single folder. * fix: escape pref values in loadFallbackHtml to prevent XSS Pref values substituted into HTML value= attributes with no escaping — a stored value containing "><script> would render in the privileged SWT Browser. Added htmlAttr() helper (escapes & , ", <) and applied to CLI_PATH and CLI_BASE_DOWNLOAD_URL substitutions. * fix: document htmlAttr scope; log unknown folder field types - Add Javadoc on htmlAttr() restricting it to double-quoted attribute context only (reviewer finding: brittle if reused in JS/URL context) - Log unknown folder field type before falling through to asText() so future LS schema additions produce a visible diagnostic * fix: substitute custom channel placeholders in loadFallbackHtml {{CHANNEL_CUSTOM_SELECTED}}, {{CLI_RELEASE_CHANNEL_CUSTOM_HIDDEN}}, and {{CLI_RELEASE_CHANNEL_CUSTOM_VALUE}} were never replaced, so custom-channel users saw literal placeholder text in the fallback form. Also introduces Preferences.RELEASE_CHANNEL_STABLE/RC/PREVIEW constants to replace the raw "stable"/"rc"/"preview" literals scattered across HTMLSettingsPreferencePage, Preferences.getReleaseChannel(), and LsSettingsRegistry. * fix: extract duplicate "selected" literal to SELECTED constant (PMD)
1 parent f873be9 commit b4655f4

5 files changed

Lines changed: 194 additions & 145 deletions

File tree

plugin/src/main/java/io/snyk/eclipse/plugin/preferences/HTMLSettingsPreferencePage.java

Lines changed: 117 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,27 @@
11
package io.snyk.eclipse.plugin.preferences;
22

33
import com.fasterxml.jackson.core.JsonProcessingException;
4+
import com.fasterxml.jackson.databind.JsonNode;
45
import com.fasterxml.jackson.databind.ObjectMapper;
56
import io.snyk.eclipse.plugin.html.BaseHtmlProvider;
67
import io.snyk.eclipse.plugin.html.ExecuteCommandBridge;
7-
import io.snyk.eclipse.plugin.properties.FolderConfigs;
8+
import io.snyk.eclipse.plugin.properties.FolderConfigSettings;
89
import io.snyk.eclipse.plugin.utils.SnykLogger;
910
import io.snyk.eclipse.plugin.views.snyktoolview.handlers.IHandlerCommands;
11+
import io.snyk.languageserver.LsFolderSettingsKeys;
12+
import io.snyk.languageserver.LsSettingsRegistry;
13+
import io.snyk.languageserver.LsSettingsRegistry.Entry;
1014
import io.snyk.languageserver.protocolextension.SnykExtendedLanguageClient;
11-
import io.snyk.languageserver.protocolextension.messageObjects.FolderConfig;
1215
import io.snyk.languageserver.protocolextension.messageObjects.ScanCommandConfig;
13-
import java.io.File;
1416
import java.io.IOException;
1517
import java.io.InputStream;
1618
import java.nio.charset.StandardCharsets;
17-
import java.nio.file.Paths;
1819
import java.util.HashMap;
1920
import java.util.List;
2021
import java.util.Map;
2122
import java.util.concurrent.CompletableFuture;
23+
import java.util.stream.Collectors;
24+
import java.util.stream.StreamSupport;
2225
import org.eclipse.jface.preference.PreferencePage;
2326
import org.eclipse.swt.SWT;
2427
import org.eclipse.swt.browser.Browser;
@@ -34,6 +37,7 @@
3437

3538
public class HTMLSettingsPreferencePage extends PreferencePage implements IWorkbenchPreferencePage {
3639

40+
private static final String SELECTED = "selected";
3741
private static volatile HTMLSettingsPreferencePage instance;
3842
private Browser browser;
3943
private final ObjectMapper objectMapper = new ObjectMapper();
@@ -153,16 +157,26 @@ private String loadFallbackHtml() {
153157
.replace("{{MANAGE_BINARIES_CHECKED}}", prefs.isManagedBinaries() ? "checked" : "")
154158
.replace(
155159
"{{CLI_BASE_DOWNLOAD_URL}}",
156-
prefs.getPref(Preferences.CLI_BASE_URL, "https://downloads.snyk.io"))
157-
.replace("{{CLI_PATH}}", prefs.getCliPath())
160+
htmlAttr(prefs.getPref(Preferences.CLI_BASE_URL, "https://downloads.snyk.io")))
161+
.replace("{{CLI_PATH}}", htmlAttr(prefs.getCliPath()))
158162
.replace(
159163
"{{CHANNEL_STABLE_SELECTED}}",
160-
"stable".equals(prefs.getReleaseChannel()) ? "selected" : "")
164+
Preferences.RELEASE_CHANNEL_STABLE.equals(prefs.getReleaseChannel()) ? SELECTED : "")
161165
.replace(
162-
"{{CHANNEL_RC_SELECTED}}", "rc".equals(prefs.getReleaseChannel()) ? "selected" : "")
166+
"{{CHANNEL_RC_SELECTED}}",
167+
Preferences.RELEASE_CHANNEL_RC.equals(prefs.getReleaseChannel()) ? SELECTED : "")
163168
.replace(
164169
"{{CHANNEL_PREVIEW_SELECTED}}",
165-
"preview".equals(prefs.getReleaseChannel()) ? "selected" : "")
170+
Preferences.RELEASE_CHANNEL_PREVIEW.equals(prefs.getReleaseChannel()) ? SELECTED : "")
171+
.replace(
172+
"{{CHANNEL_CUSTOM_SELECTED}}",
173+
isCustomChannel(prefs.getReleaseChannel()) ? SELECTED : "")
174+
.replace(
175+
"{{CLI_RELEASE_CHANNEL_CUSTOM_HIDDEN}}",
176+
isCustomChannel(prefs.getReleaseChannel()) ? "" : "hidden")
177+
.replace(
178+
"{{CLI_RELEASE_CHANNEL_CUSTOM_VALUE}}",
179+
isCustomChannel(prefs.getReleaseChannel()) ? htmlAttr(prefs.getReleaseChannel()) : "")
166180
.replace("{{INSECURE_CHECKED}}", prefs.isInsecure() ? "checked" : "");
167181
} catch (IOException e) {
168182
SnykLogger.logError(e);
@@ -172,136 +186,119 @@ private String loadFallbackHtml() {
172186

173187
private void parseAndSaveConfig(String jsonString) {
174188
try {
175-
IdeConfigData config = objectMapper.readValue(jsonString, IdeConfigData.class);
189+
JsonNode root = objectMapper.readTree(jsonString);
176190
Preferences prefs = Preferences.getInstance();
177191

178-
boolean isFallback = Boolean.TRUE.equals(config.isFallbackForm());
179-
180-
// CLI Settings - always persist for both fallback and full forms
181-
prefs.store(Preferences.CLI_PATH, String.valueOf(config.cliPath()));
182-
prefs.store(
183-
Preferences.MANAGE_BINARIES_AUTOMATICALLY,
184-
String.valueOf(config.manageBinariesAutomatically()));
185-
prefs.store(Preferences.CLI_BASE_URL, String.valueOf(config.cliBaseDownloadURL()));
186-
prefs.store(Preferences.RELEASE_CHANNEL, String.valueOf(config.cliReleaseChannel()));
187-
prefs.store(Preferences.INSECURE_KEY, String.valueOf(config.insecure()));
188-
189-
// Only persist non-CLI fields if not fallback form
190-
if (!isFallback) {
191-
// Scan Settings
192-
prefs.store(
193-
Preferences.ACTIVATE_SNYK_OPEN_SOURCE,
194-
String.valueOf(config.activateSnykOpenSource()));
195-
prefs.store(
196-
Preferences.ACTIVATE_SNYK_CODE_SECURITY, String.valueOf(config.activateSnykCode()));
197-
prefs.store(Preferences.ACTIVATE_SNYK_IAC, String.valueOf(config.activateSnykIac()));
198-
199-
if (config.scanningMode() != null) {
200-
boolean isAutomatic = "auto".equals(config.scanningMode());
201-
prefs.store(Preferences.SCANNING_MODE_AUTOMATIC, String.valueOf(isAutomatic));
202-
}
203-
204-
// Issue View Settings
205-
if (config.issueViewOptions() != null) {
206-
IdeConfigData.IssueViewOptions options = config.issueViewOptions();
207-
prefs.store(
208-
Preferences.FILTER_IGNORES_SHOW_OPEN_ISSUES, String.valueOf(options.openIssues()));
209-
prefs.store(
210-
Preferences.FILTER_IGNORES_SHOW_IGNORED_ISSUES,
211-
String.valueOf(options.ignoredIssues()));
212-
}
213-
prefs.store(Preferences.ENABLE_DELTA, String.valueOf(config.enableDeltaFindings()));
214-
215-
// Authentication Settings
216-
if (config.authenticationMethod() != null) {
217-
prefs.store(Preferences.AUTHENTICATION_METHOD, config.authenticationMethod());
192+
JsonNode fallbackNode = root.get("isFallbackForm");
193+
boolean isFallback = fallbackNode != null && fallbackNode.booleanValue();
194+
195+
for (Entry entry : LsSettingsRegistry.ENTRIES.values()) {
196+
if (entry.prefKey == null) continue;
197+
if (isFallback && !entry.useInFallbackForm) continue;
198+
199+
JsonNode n = root.get(entry.lsKey.key);
200+
if (n == null) {
201+
// absent — form didn't send this key, leave tracking untouched
202+
} else if (n.isNull()) {
203+
prefs.clearExplicitlyChangedNoFlush(entry.prefKey);
204+
} else if (entry.formDeserializer != null) {
205+
prefs.store(entry.prefKey, entry.formDeserializer.apply(n));
206+
prefs.markExplicitlyChangedNoFlush(entry.prefKey);
207+
} else {
208+
prefs.store(entry.prefKey, nodeToString(n));
209+
prefs.markExplicitlyChangedNoFlush(entry.prefKey);
218210
}
211+
}
212+
prefs.flushExplicitChanges();
219213

220-
// Connection Settings
221-
prefs.store(Preferences.ENDPOINT_KEY, String.valueOf(config.endpoint()));
222-
prefs.store(Preferences.AUTH_TOKEN_KEY, String.valueOf(config.token()));
223-
if (config.organization() != null) {
224-
prefs.store(Preferences.ORGANIZATION_KEY, String.valueOf(config.organization()));
225-
}
226-
227-
// Trusted Folders
228-
if (config.trustedFolders() != null) {
229-
String trustedFoldersString = String.join(File.pathSeparator, config.trustedFolders());
230-
prefs.store(Preferences.TRUSTED_FOLDERS, trustedFoldersString);
231-
}
232-
233-
// Filter Settings
234-
if (config.filterSeverity() != null) {
235-
IdeConfigData.FilterSeverity severity = config.filterSeverity();
236-
prefs.store(Preferences.FILTER_SHOW_CRITICAL, String.valueOf(severity.critical()));
237-
prefs.store(Preferences.FILTER_SHOW_HIGH, String.valueOf(severity.high()));
238-
prefs.store(Preferences.FILTER_SHOW_MEDIUM, String.valueOf(severity.medium()));
239-
prefs.store(Preferences.FILTER_SHOW_LOW, String.valueOf(severity.low()));
240-
}
241-
prefs.store(
242-
Preferences.RISK_SCORE_THRESHOLD, String.valueOf(config.riskScoreThreshold()));
243-
244-
// Folder Configs
245-
if (config.folderConfigs() != null && !config.folderConfigs().isEmpty()) {
246-
for (IdeConfigData.FolderConfigData folderConfigData : config.folderConfigs()) {
247-
processFolderConfig(folderConfigData);
214+
if (!isFallback) {
215+
// Folder configs.
216+
JsonNode folderConfigsNode = root.get("folderConfigs");
217+
if (folderConfigsNode != null && folderConfigsNode.isArray()) {
218+
for (JsonNode folderNode : folderConfigsNode) {
219+
processFolderConfig(folderNode);
248220
}
249221
}
250222
}
251223

252-
// Refresh toolbar UI to reflect changes made in HTML settings
224+
// Refresh toolbar UI to reflect changes made in HTML settings.
253225
refreshToolbarUI();
254226
} catch (JsonProcessingException e) {
255227
SnykLogger.logError(e);
256228
}
257229
}
258230

259-
private void processFolderConfig(IdeConfigData.FolderConfigData folderConfigData) {
260-
if (folderConfigData.folderPath() == null) {
261-
return;
231+
private static String nodeToString(JsonNode node) {
232+
if (node.isBoolean()) {
233+
return node.asText(); // "true" or "false"
262234
}
263-
264-
FolderConfig folderConfig =
265-
FolderConfigs.getInstance().getFolderConfig(Paths.get(folderConfigData.folderPath()));
266-
267-
if (folderConfigData.preferredOrg() != null) {
268-
folderConfig.setPreferredOrg(folderConfigData.preferredOrg());
269-
}
270-
if (folderConfigData.autoDeterminedOrg() != null) {
271-
folderConfig.setAutoDeterminedOrg(folderConfigData.autoDeterminedOrg());
272-
}
273-
if (folderConfigData.orgSetByUser() != null) {
274-
folderConfig.setOrgSetByUser(folderConfigData.orgSetByUser());
275-
}
276-
if (folderConfigData.additionalEnv() != null) {
277-
folderConfig.setAdditionalEnv(folderConfigData.additionalEnv());
278-
}
279-
if (folderConfigData.additionalParameters() != null) {
280-
folderConfig.setAdditionalParameters(folderConfigData.additionalParameters());
281-
}
282-
if (folderConfigData.scanCommandConfig() != null) {
283-
Map<String, ScanCommandConfig> targetConfigMap =
284-
convertScanCommandConfig(folderConfigData.scanCommandConfig());
285-
folderConfig.setScanCommandConfig(targetConfigMap);
235+
if (node.isNumber()) {
236+
double d = node.asDouble();
237+
if (d == Math.floor(d) && !Double.isInfinite(d)) {
238+
return String.valueOf((long) d);
239+
}
240+
return String.valueOf(d);
286241
}
242+
return node.asText();
287243
}
288244

289-
@SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
290-
private Map<String, ScanCommandConfig> convertScanCommandConfig(
291-
Map<String, IdeConfigData.ScanCommandConfigData> sourceConfigMap) {
292-
Map<String, ScanCommandConfig> targetConfigMap = new HashMap<>();
293-
for (Map.Entry<String, IdeConfigData.ScanCommandConfigData> entry :
294-
sourceConfigMap.entrySet()) {
295-
IdeConfigData.ScanCommandConfigData configData = entry.getValue();
296-
ScanCommandConfig scanCommandConfig =
297-
new ScanCommandConfig(
298-
configData.preScanCommand(),
299-
Boolean.TRUE.equals(configData.preScanOnlyReferenceFolder()),
300-
configData.postScanCommand(),
301-
Boolean.TRUE.equals(configData.postScanOnlyReferenceFolder()));
302-
targetConfigMap.put(entry.getKey(), scanCommandConfig);
245+
private void processFolderConfig(JsonNode folderNode) {
246+
JsonNode pathNode = folderNode.get("folderPath");
247+
if (pathNode == null || pathNode.isNull()) {
248+
return;
303249
}
304-
return targetConfigMap;
250+
String pathStr = pathNode.asText();
251+
FolderConfigSettings.getInstance().computeFolderConfig(pathStr, config -> {
252+
var fields = folderNode.fields();
253+
while (fields.hasNext()) {
254+
var field = fields.next();
255+
String key = field.getKey();
256+
JsonNode node = field.getValue();
257+
if ("folderPath".equals(key) || node.isNull()) {
258+
continue;
259+
}
260+
if (LsFolderSettingsKeys.SCAN_COMMAND_CONFIG.equals(key)) {
261+
if (!node.isObject()) {
262+
SnykLogger.logInfo("Skipping non-object scan_command_config for folder " + pathStr);
263+
continue;
264+
}
265+
try {
266+
Map<String, ScanCommandConfig> scanCommandMap = objectMapper.convertValue(
267+
node, objectMapper.getTypeFactory().constructMapType(HashMap.class, String.class, ScanCommandConfig.class));
268+
config = config.withSetting(key, scanCommandMap, true);
269+
} catch (IllegalArgumentException e) {
270+
SnykLogger.logError(e);
271+
continue;
272+
}
273+
} else if (node.isArray()) {
274+
List<String> list = StreamSupport.stream(node.spliterator(), false)
275+
.filter(el -> !el.isNull())
276+
.map(JsonNode::asText)
277+
.collect(Collectors.toList());
278+
config = config.withSetting(key, list, true);
279+
} else if (node.isBoolean()) {
280+
config = config.withSetting(key, node.booleanValue(), true);
281+
} else {
282+
SnykLogger.logInfo("processFolderConfig: storing unknown field type as text for key=" + key
283+
+ " jsonType=" + node.getNodeType());
284+
config = config.withSetting(key, node.asText(), true);
285+
}
286+
}
287+
return config;
288+
});
289+
}
290+
291+
private static boolean isCustomChannel(String channel) {
292+
return channel != null
293+
&& !Preferences.RELEASE_CHANNEL_STABLE.equals(channel)
294+
&& !Preferences.RELEASE_CHANNEL_RC.equals(channel)
295+
&& !Preferences.RELEASE_CHANNEL_PREVIEW.equals(channel);
296+
}
297+
298+
/** Escapes for double-quoted HTML attribute context only. Do not use in JS/URL contexts. */
299+
private static String htmlAttr(String v) {
300+
if (v == null) return "";
301+
return v.replace("&", "&amp;").replace("\"", "&quot;").replace("<", "&lt;");
305302
}
306303

307304
private void refreshToolbarUI() {

plugin/src/main/java/io/snyk/eclipse/plugin/preferences/Preferences.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,9 @@ public class Preferences {
8686
public static final String DEFAULT_ENDPOINT = "https://api.snyk.io";
8787
public static final String DEVICE_ID = "deviceId";
8888
public static final String RELEASE_CHANNEL = "releaseChannel";
89+
public static final String RELEASE_CHANNEL_STABLE = "stable";
90+
public static final String RELEASE_CHANNEL_RC = "rc";
91+
public static final String RELEASE_CHANNEL_PREVIEW = "preview";
8992
public static final String USE_LS_HTML_CONFIG_DIALOG = "useLsHtmlConfigDialog";
9093
public static final String EXPLICIT_CHANGES_KEY = "explicitChanges";
9194

@@ -342,7 +345,7 @@ public final boolean getBooleanPref(String key, boolean defaultValue) {
342345
}
343346

344347
public final String getReleaseChannel() {
345-
return getPref(RELEASE_CHANNEL, "stable");
348+
return getPref(RELEASE_CHANNEL, RELEASE_CHANNEL_STABLE);
346349
}
347350

348351
public void setTest(boolean b) {

plugin/src/main/java/io/snyk/languageserver/LsSettingsRegistry.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ static Entry encryptedAlwaysChanged(LsKey lsKey, String prefKey, String outbound
202202
}
203203
return sb.toString();
204204
}, false));
205-
entries.put(LsKey.CLI_RELEASE_CHANNEL, Entry.fallback(LsKey.CLI_RELEASE_CHANNEL, Preferences.RELEASE_CHANNEL, "stable"));
205+
entries.put(LsKey.CLI_RELEASE_CHANNEL, Entry.fallback(LsKey.CLI_RELEASE_CHANNEL, Preferences.RELEASE_CHANNEL, Preferences.RELEASE_CHANNEL_STABLE));
206206

207207
ENTRIES = Collections.unmodifiableMap(entries);
208208

plugin/src/main/java/io/snyk/languageserver/protocolextension/SnykExtendedLanguageClient.java

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -448,10 +448,7 @@ private void persistGlobalSettings(java.util.Map<String, ConfigSetting> settings
448448
for (var entry : settings.entrySet()) {
449449
try {
450450
var registryEntry = LsSettingsRegistry.BY_LS_KEY.get(entry.getKey());
451-
if (registryEntry == null || registryEntry.prefKey == null) {
452-
continue;
453-
}
454-
if (registryEntry.lsKey == LsKey.TOKEN) {
451+
if (registryEntry == null || registryEntry.prefKey == null || registryEntry.encrypted) {
455452
continue;
456453
}
457454
var setting = entry.getValue();

0 commit comments

Comments
 (0)