}
- *
- * In a Windows batch file, you can get this with:
- * reg query HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography /v MachineGuid
- */
-function _getWindowsDeviceID() {
- return new Promise((resolve, reject) => {
- exec(
- 'reg query HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Cryptography /v MachineGuid',
- { encoding: 'utf8' },
- (err, stdout) => {
- if (err) {
- console.error('Failed to get Windows device ID:', err.message);
- return reject(err);
- }
-
- // Example output:
- // HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography
- // MachineGuid REG_SZ 4c4c4544-0034-5a10-8051-cac04f305a31
- const match = stdout.match(/MachineGuid\s+REG_[A-Z]+\s+([a-fA-F0-9-]+)/);
- if (match && match[1]) {
- resolve(match[1].trim());
- } else {
- resolve(null);
- }
- }
- );
- });
-}
-
-async function getDeviceID() {
- if (process.platform === "linux") {
- return _getLinuxDeviceID();
- } else if (process.platform === "darwin") {
- return _getMacDeviceID();
- } else if (process.platform === "win32") {
- return _getWindowsDeviceID();
- }
- throw new Error(`Unsupported platform: ${process.platform}`);
+async function getSystemSettingsDir() {
+ return SYSTEM_SETTINGS_DIR;
}
exports.getURLContent = getURLContent;
@@ -456,5 +292,7 @@ exports.addDeviceLicense = addDeviceLicense;
exports.removeDeviceLicense = removeDeviceLicense;
exports.isLicensedDevice = isLicensedDevice;
exports.getDeviceID = getDeviceID;
+exports.getOSUserName = getOSUserName;
+exports.getSystemSettingsDir = getSystemSettingsDir;
exports._loadNodeExtensionModule = _loadNodeExtensionModule;
exports._npmInstallInFolder = _npmInstallInFolder;
diff --git a/src/config.json b/src/config.json
index 393c4010fd..c60bc4f6e6 100644
--- a/src/config.json
+++ b/src/config.json
@@ -29,6 +29,7 @@
"extension_store_url": "https://store.core.ai/src/",
"app_notification_url": "assets/notifications/dev/",
"app_update_url": "https://updates.phcode.io/tauri/update-latest-experimental-build.json",
+ "extensionTakedownURL": "https://updates.phcode.io/extension_takedown.json",
"linting.enabled_by_default": true,
"build_timestamp": "",
"googleAnalyticsID": "G-P4HJFPDB76",
diff --git a/src/extensibility/ExtensionManager.js b/src/extensibility/ExtensionManager.js
index 70953fa292..68fd3e99fc 100644
--- a/src/extensibility/ExtensionManager.js
+++ b/src/extensibility/ExtensionManager.js
@@ -1006,6 +1006,7 @@ define(function (require, exports, module) {
exports.updateExtensions = updateExtensions;
exports.getAvailableUpdates = getAvailableUpdates;
exports.cleanAvailableUpdates = cleanAvailableUpdates;
+ exports.isExtensionTakenDown = ExtensionLoader.isExtensionTakenDown;
exports.ENABLED = ENABLED;
exports.DISABLED = DISABLED;
diff --git a/src/extensibility/ExtensionManagerView.js b/src/extensibility/ExtensionManagerView.js
index 842b1e14f3..4af6f0b9ee 100644
--- a/src/extensibility/ExtensionManagerView.js
+++ b/src/extensibility/ExtensionManagerView.js
@@ -43,8 +43,8 @@ define(function (require, exports, module) {
PathUtils = require("thirdparty/path-utils/path-utils"),
itemTemplate = require("text!htmlContent/extension-manager-view-item.html"),
PreferencesManager = require("preferences/PreferencesManager"),
- warnExtensionIDs = JSON.parse(require("text!extensions/default/DefaultExtensions.json"))
- .warnExtensionStoreExtensions.extensionIDs,
+ DefaultExtensions = JSON.parse(require("text!extensions/default/DefaultExtensions.json")),
+ warnExtensionIDs = new Set(DefaultExtensions.warnExtensionStoreExtensions.extensionIDs),
Metrics = require("utils/Metrics");
@@ -363,7 +363,8 @@ define(function (require, exports, module) {
context.isCurrentTheme = entry.installInfo &&
(entry.installInfo.metadata.name === ThemeManager.getCurrentTheme().name);
- context.defaultFeature = warnExtensionIDs.includes(info.metadata.name);
+ context.defaultFeature = warnExtensionIDs.has(info.metadata.name);
+ context.isDeprecatedExtension = ExtensionManager.isExtensionTakenDown(info.metadata.name);
context.allowInstall = context.isCompatible && !context.isInstalled;
@@ -419,9 +420,9 @@ define(function (require, exports, module) {
var isDefaultOrInstalled = this.model.source === "default" || this.model.source === "installed";
var isDefaultAndTheme = this.model.source === "default" && context.metadata.theme;
context.disablingAllowed = isDefaultOrInstalled && !isDefaultAndTheme && !context.disabled
- && !hasPendingAction && !context.metadata.theme;
+ && !hasPendingAction && !context.metadata.theme && !context.isDeprecatedExtension;
context.enablingAllowed = isDefaultOrInstalled && !isDefaultAndTheme && context.disabled
- && !hasPendingAction && !context.metadata.theme;
+ && !hasPendingAction && !context.metadata.theme && !context.isDeprecatedExtension;
// Copy over helper functions that we share with the registry app.
["lastVersionDate", "authorInfo"].forEach(function (helper) {
diff --git a/src/extensibility/ExtensionManagerViewModel.js b/src/extensibility/ExtensionManagerViewModel.js
index 0cc62781b5..98bf3a246c 100644
--- a/src/extensibility/ExtensionManagerViewModel.js
+++ b/src/extensibility/ExtensionManagerViewModel.js
@@ -26,11 +26,11 @@ define(function (require, exports, module) {
var _ = require("thirdparty/lodash");
- var ExtensionManager = require("extensibility/ExtensionManager"),
- registry_utils = require("extensibility/registry_utils"),
- EventDispatcher = require("utils/EventDispatcher"),
- Strings = require("strings"),
- PreferencesManager = require("preferences/PreferencesManager");
+ const ExtensionManager = require("extensibility/ExtensionManager"),
+ registry_utils = require("extensibility/registry_utils"),
+ EventDispatcher = require("utils/EventDispatcher"),
+ Strings = require("strings"),
+ PreferencesManager = require("preferences/PreferencesManager");
/**
* @private
@@ -307,6 +307,9 @@ define(function (require, exports, module) {
return entry.registryInfo && entry.registryInfo.metadata.theme;
})
+ .filter(function (entry) {
+ return !ExtensionManager.isExtensionTakenDown(entry.registryInfo.metadata.name);
+ })
.map(function (entry) {
return entry.registryInfo.metadata.name;
});
@@ -555,6 +558,9 @@ define(function (require, exports, module) {
.filter(function (key) {
return self.extensions[key].installInfo &&
self.extensions[key].installInfo.locationType === ExtensionManager.LOCATION_DEFAULT;
+ })
+ .filter(function (key) {
+ return !ExtensionManager.isExtensionTakenDown(key);
});
this._sortFullSet();
this._setInitialFilter();
diff --git a/src/extensions/default/DefaultExtensions.json b/src/extensions/default/DefaultExtensions.json
index fd1fc5483f..8de57731af 100644
--- a/src/extensions/default/DefaultExtensions.json
+++ b/src/extensions/default/DefaultExtensions.json
@@ -36,5 +36,8 @@
"bib-locked-live-preview", "brackets-display-shortcuts", "changing-tags", "brackets-indent-guides", "brackets-emmet",
"github-Pluto-custom-line-height"
]
- }
+ },
+ "dontLoadExtensions": {
+ "note": "To take down any compromised extensions, see `extensionTakedownURL` in repo"
+ }
}
diff --git a/src/htmlContent/extension-manager-view-item.html b/src/htmlContent/extension-manager-view-item.html
index 6f31e4e438..893ea928bd 100644
--- a/src/htmlContent/extension-manager-view-item.html
+++ b/src/htmlContent/extension-manager-view-item.html
@@ -39,6 +39,11 @@
|
+ {{#isDeprecatedExtension}}
+ {{#isInstalled}}
+ {{Strings.EXTENSION_DEPRECATED_NOT_LOADED}}
+ {{/isInstalled}}
+ {{/isDeprecatedExtension}}
{{#showInstallButton}}
{{#defaultFeature}}
diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js
index 0c148a9cdb..2862b95152 100644
--- a/src/nls/root/strings.js
+++ b/src/nls/root/strings.js
@@ -809,6 +809,7 @@ define({
"EXTENSION_INCOMPATIBLE_NEWER": "This extension requires a newer version of {APP_NAME}.",
"EXTENSION_INCOMPATIBLE_OLDER": "This extension currently only works with older versions of {APP_NAME}.",
"EXTENSION_DEFAULT_FEATURE_PRESENT": "You may not need this extension. {APP_NAME} already has this feature.",
+ "EXTENSION_DEPRECATED_NOT_LOADED": "Extension not loaded. It is either deprecated or insecure.",
"EXTENSION_LATEST_INCOMPATIBLE_NEWER": "Version {0} of this extension requires a newer version of {APP_NAME}. But you can install the earlier version {1}.",
"EXTENSION_LATEST_INCOMPATIBLE_OLDER": "Version {0} of this extension only works with older versions of {APP_NAME}. But you can install the earlier version {1}.",
"EXTENSION_NO_DESCRIPTION": "No description",
diff --git a/src/utils/ExtensionLoader.js b/src/utils/ExtensionLoader.js
index 52ae93d3b4..35d793436c 100644
--- a/src/utils/ExtensionLoader.js
+++ b/src/utils/ExtensionLoader.js
@@ -56,6 +56,96 @@ define(function (require, exports, module) {
PathUtils = require("thirdparty/path-utils/path-utils"),
DefaultExtensions = JSON.parse(require("text!extensions/default/DefaultExtensions.json"));
+ // takedown/dont load extensions that are compromised at app start - start
+ const EXTENSION_TAKEDOWN_LOCALSTORAGE_KEY = "PH_EXTENSION_TAKEDOWN_LIST";
+
+ function _getTakedownListLS() {
+ try{
+ let list = localStorage.getItem(EXTENSION_TAKEDOWN_LOCALSTORAGE_KEY);
+ if(list) {
+ list = JSON.parse(list);
+ if (Array.isArray(list)) {
+ return list;
+ }
+ }
+ } catch (e) {
+ console.error(e);
+ }
+ return [];
+ }
+
+ const loadedExtensionIDs = new Set();
+ let takedownExtensionList = new Set(_getTakedownListLS());
+
+ const EXTENSION_TAKEDOWN_URL = brackets.config.extensionTakedownURL;
+
+ function _anyTakenDownExtensionLoaded() {
+ if (takedownExtensionList.size === 0 || loadedExtensionIDs.size === 0) {
+ return [];
+ }
+ let smaller;
+ let larger;
+
+ if (takedownExtensionList.size < loadedExtensionIDs.size) {
+ smaller = takedownExtensionList;
+ larger = loadedExtensionIDs;
+ } else {
+ smaller = loadedExtensionIDs;
+ larger = takedownExtensionList;
+ }
+
+ const matches = [];
+
+ for (const id of smaller) {
+ if (larger.has(id)) {
+ matches.push(id);
+ }
+ }
+
+ return matches;
+ }
+
+ function fetchWithTimeout(url, ms) {
+ const c = new AbortController();
+ const t = setTimeout(() => c.abort(), ms);
+ return fetch(url, { signal: c.signal }).finally(() => clearTimeout(t));
+ }
+
+ // we dont want a restart after user does too much in the app causing data loss. So we wont reload after 20 seconds.
+ fetchWithTimeout(EXTENSION_TAKEDOWN_URL, 20000)
+ .then(response => {
+ if (!response.ok) {
+ throw new Error(`HTTP ${response.status} - ${response.statusText}`);
+ }
+ return response.json();
+ })
+ .then(data => {
+ console.log('Extension takedown data:', data);
+ if (!Array.isArray(data) || !data.every(x => typeof x === "string")) {
+ console.error("Takedown list must be an array of strings.");
+ return;
+ }
+ const dataToWrite = JSON.stringify(data);
+ localStorage.setItem(EXTENSION_TAKEDOWN_LOCALSTORAGE_KEY, dataToWrite);
+ takedownExtensionList = new Set(data);
+ const compromisedExtensionsLoaded = _anyTakenDownExtensionLoaded();
+ if(!compromisedExtensionsLoaded.length){
+ return;
+ }
+ // if we are here, we have already loaded some compromised extensions. we need to reload app as soon as
+ // possible. no await after this. all sync js calls to prevent extension from tampering with this list.
+ const writtenData = localStorage.getItem(EXTENSION_TAKEDOWN_LOCALSTORAGE_KEY);
+ if(writtenData !== dataToWrite) {
+ // the write did not succeded. local storage write can fail if storage full, if so we may cause infinite
+ // reloads here if we dont do the check.
+ console.error("Failed to write taken down extension to localstorage");
+ return;
+ }
+ location.reload();
+ })
+ .catch(console.error);
+ // takedown/dont load extensions that are compromised at app start - end
+
const desktopOnlyExtensions = DefaultExtensions.desktopOnly;
const DefaultExtensionsList = Phoenix.isNativeApp ?
[...DefaultExtensions.defaultExtensionsList, ...desktopOnlyExtensions]:
@@ -416,6 +506,16 @@ define(function (require, exports, module) {
return promise
.then(function (metadata) {
+ if (isExtensionTakenDown(metadata.name)) {
+ logger.leaveTrail("skip load taken down extension: " + metadata.name);
+ console.warn("skip load taken down extension: " + metadata.name);
+ return new $.Deferred().reject("disabled").promise();
+ }
+
+ if(metadata.name) {
+ loadedExtensionIDs.add(metadata.name);
+ }
+
// No special handling for themes... Let the promise propagate into the ExtensionManager
if (metadata && metadata.theme) {
return;
@@ -923,6 +1023,15 @@ define(function (require, exports, module) {
return promise;
}
+ function isExtensionTakenDown(extensionID) {
+ if(!extensionID){
+ // extensions without id can happen with local development. these are never distributed in store.
+ // so safe to return false here.
+ return false;
+ }
+ return takedownExtensionList.has(extensionID);
+ }
+
EventDispatcher.makeEventDispatcher(exports);
@@ -946,6 +1055,7 @@ define(function (require, exports, module) {
exports.testExtension = testExtension;
exports.loadAllExtensionsInNativeDirectory = loadAllExtensionsInNativeDirectory;
exports.loadExtensionFromNativeDirectory = loadExtensionFromNativeDirectory;
+ exports.isExtensionTakenDown = isExtensionTakenDown;
exports.testAllExtensionsInNativeDirectory = testAllExtensionsInNativeDirectory;
exports.testAllDefaultExtensions = testAllDefaultExtensions;
exports.EVENT_EXTENSION_LOADED = EVENT_EXTENSION_LOADED;
diff --git a/src/utils/NodeUtils.js b/src/utils/NodeUtils.js
index b76d546842..38e8c5325b 100644
--- a/src/utils/NodeUtils.js
+++ b/src/utils/NodeUtils.js
@@ -275,6 +275,37 @@ define(function (require, exports, module) {
return false;
}
+ /**
+ * Retrieves the operating system username of the current user.
+ * This method is only available in native apps.
+ *
+ * @throws {Error} Throws an error if called in a browser environment.
+ * @return {Promise} A promise that resolves to the OS username of the current user.
+ */
+ async function getOSUserName() {
+ if (!Phoenix.isNativeApp) {
+ throw new Error("getOSUserName not available in browser");
+ }
+ return utilsConnector.execPeer("getOSUserName");
+ }
+
+ let _systemSettingsDir;
+ /**
+ * Retrieves the directory path for system settings. This method is applicable to native apps only.
+ *
+ * @return {Promise} A promise that resolves to the path of the system settings directory.
+ * @throws {Error} If the method is called in browser app.
+ */
+ async function getSystemSettingsDir() {
+ if (!Phoenix.isNativeApp) {
+ throw new Error("getSystemSettingsDir is sudo folder is win/linux/mac. not available in browser.");
+ }
+ if(!_systemSettingsDir){
+ _systemSettingsDir = await utilsConnector.execPeer("getSystemSettingsDir");
+ }
+ return _systemSettingsDir;
+ }
+
if(NodeConnector.isNodeAvailable()) {
// todo we need to update the strings if a user extension adds its translations. Since we dont support
// node extensions for now, should consider when we support node extensions.
@@ -317,6 +348,8 @@ define(function (require, exports, module) {
exports.removeDeviceLicenseSystemWide = removeDeviceLicenseSystemWide;
exports.isLicensedDeviceSystemWide = isLicensedDeviceSystemWide;
exports.getDeviceID = getDeviceID;
+ exports.getOSUserName = getOSUserName;
+ exports.getSystemSettingsDir = getSystemSettingsDir;
/**
* checks if Node connector is ready
|