Skip to content

Commit b291d00

Browse files
authored
[BridgeLink] Add better support for composite plugins (#120)
* Initial version with the drop down menu * Make the menu update after each state change * Reload the page after activating or deactivating bridge link * Formatting changes * Make sure that any callsign for the composite plugin will work, as well as any number of pararell composite plugins running with many Thunders * Intercept each API call and add a prefix to calls from composite Thunder instances * Clean everything up, make sure one nesting layer is allowed and you need to use another UI to chain more Thunders * Clean the formatting and comments * Make sure that active UI of the plugin survives the page reload * Thunder instances as buttons on the top of the page instead of a drop-down menu * Fix a bug with inproper caching between Thunder instances * Slight formatting changes * Update some comments * Fix the Cross-site scripting vulnerability found by the scanning tools * Try once again to fix the code scanning issues * Build header using DOM methods to avoid innerHTML XSS concerns * Take care of one other innerHTML * Replace all innerHTML in Controller.js with DOM methods * Sanitaze the data and fix one more innerHTML in application.js
1 parent 6d7e10f commit b291d00

6 files changed

Lines changed: 874 additions & 137 deletions

File tree

dist/bundle.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/css/style.css

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ html {
5858
user-select: none;
5959
z-index: 1000;
6060
background-color: rgba(0,0,0,0.1);
61+
display: flex;
62+
align-items: center;
6163
}
6264

6365
.touch .header img {
@@ -633,10 +635,8 @@ div#hide-notifications:hover {
633635

634636
.desktop .header img {
635637
height: 30px;
636-
width: 30px;
637-
margin-top: 15px;
638-
margin-left: 35px;
639-
margin-bottom: 10px;
638+
width: auto;
639+
margin-left: 15px;
640640
}
641641

642642
@media screen and (min-width: 960px) {
@@ -882,3 +882,43 @@ input[type=checkbox] {
882882
-moz-transition: transform 0.15s ease-in-out, background-color 0.15s ease-in-out;
883883
-webkit-transition: transform 0.15s ease-in-out, background-color 0.15s ease-in-out;
884884
}
885+
886+
.instance-buttons {
887+
display: flex;
888+
gap: 8px;
889+
margin-left: 20px;
890+
overflow-x: auto;
891+
max-width: calc(100vw - 250px);
892+
scrollbar-width: thin;
893+
}
894+
895+
.instance-buttons::-webkit-scrollbar {
896+
height: 4px;
897+
}
898+
899+
.instance-buttons::-webkit-scrollbar-thumb {
900+
background: rgba(255, 255, 255, 0.3);
901+
border-radius: 2px;
902+
}
903+
904+
.instance-button {
905+
background-color: #444;
906+
border: 1px solid #555;
907+
border-radius: 4px;
908+
color: #ccc;
909+
padding: 6px 14px;
910+
font-size: 13px;
911+
cursor: pointer;
912+
transition: all 0.2s ease;
913+
}
914+
915+
.instance-button:hover {
916+
background-color: #555;
917+
color: #fff;
918+
}
919+
920+
.instance-button.active {
921+
background-color: #0078d4;
922+
border-color: #0078d4;
923+
color: #fff;
924+
}

src/js/core/application.js

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,9 @@ var plugin = undefined;
4545
// private
4646
var fetchedPlugins = [];
4747
var mainDiv = document.getElementById('main');
48-
var activePlugin = window.localStorage.getItem('lastActivePlugin') || undefined;
48+
// Sanitize localStorage input to prevent stored XSS
49+
var storedPlugin = window.localStorage.getItem('lastActivePlugin');
50+
var activePlugin = storedPlugin ? sanitizeForId(storedPlugin) : undefined;
4951

5052
/**
5153
* Main initialization function
@@ -85,24 +87,47 @@ function init(host){
8587

8688
/** (global) renders a plugin in the main div */
8789
function showPlugin(callsign) {
88-
if (plugins[ callsign ] === undefined)
90+
// Extract base callsign for plugin lookup (e.g., "DeviceInfo" from "BridgeLink1/DeviceInfo")
91+
const delimiter = '/';
92+
const lastDelimiterIndex = callsign.lastIndexOf(delimiter);
93+
const baseCallsign = lastDelimiterIndex !== -1 ? callsign.substring(lastDelimiterIndex + 1) : callsign;
94+
const prefix = lastDelimiterIndex !== -1 ? callsign.substring(0, lastDelimiterIndex) : null;
95+
96+
if (plugins[ baseCallsign ] === undefined)
8997
return;
9098

91-
if (activePlugin !== undefined && plugins[ activePlugin ] !== undefined)
92-
plugins[ activePlugin ].close();
99+
if (activePlugin !== undefined) {
100+
// Get base callsign for the currently active plugin
101+
const activeLastDelimiter = activePlugin.lastIndexOf(delimiter);
102+
const activeBaseCallsign = activeLastDelimiter !== -1 ? activePlugin.substring(activeLastDelimiter + 1) : activePlugin;
103+
if (plugins[ activeBaseCallsign ] !== undefined)
104+
plugins[ activeBaseCallsign ].close();
105+
}
93106

94-
document.getElementById('main').innerHTML = '';
95-
plugins[ callsign ].render();
96107
activePlugin = callsign;
97-
window.localStorage.setItem('lastActivePlugin', callsign);
98-
};
108+
109+
// Set the active prefix on the API so all subsequent calls use it
110+
api.setActivePrefix(prefix);
111+
112+
plugins[ baseCallsign ].render();
113+
}
114+
115+
// Sanitize a string for safe use as object key/DOM id
116+
function sanitizeForId(str) {
117+
if (typeof str !== 'string') return '';
118+
return str.replace(/[^a-zA-Z0-9_\/-]/g, '_');
119+
}
99120

100121
/** (global) refresh current active plugin */
101122
function renderCurrentPlugin() {
102123
// lets re-render menu too, just to be sure
103124
plugins.menu.render(activePlugin);
104125

105-
document.getElementById('main').innerHTML = '';
126+
// Use DOM methods instead of innerHTML
127+
var main = document.getElementById('main');
128+
while (main.firstChild) {
129+
main.removeChild(main.firstChild);
130+
}
106131
plugins[ activePlugin ].render();
107132
};
108133

src/js/core/wpeApi.js

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ export default class WpeApi {
3535
this.t = ThunderJS({ 'host' : this.host[0], 'port': this.host[1] });
3636

3737
this.socketListeners = {};
38+
39+
// Active prefix for composite plugin support (e.g., "BridgeLink1")
40+
this.activePrefix = null;
3841

3942
// might use this later if the requests are getting to slow with the jsonrpc -> rest fallback.
4043
this.servicesAvailableInJsonRPC = [
@@ -61,6 +64,34 @@ export default class WpeApi {
6164
];
6265
};
6366

67+
/**
68+
* Set the active prefix for API calls.
69+
* When set, all plugin calls will be prefixed (e.g., "MessageControl" becomes "BridgeLink1/MessageControl").
70+
* @param {string|null} prefix - The prefix to apply, or null for local Thunder
71+
*/
72+
setActivePrefix(prefix) {
73+
this.activePrefix = prefix;
74+
}
75+
76+
/**
77+
* Get the prefixed plugin name for API calls.
78+
* @param {string} pluginName - The base plugin name
79+
* @returns {string} The prefixed plugin name (or original if no prefix set)
80+
*/
81+
getPrefixedPlugin(pluginName) {
82+
// Don't prefix if no active prefix
83+
if (!this.activePrefix) {
84+
return pluginName;
85+
}
86+
87+
// Don't double-prefix if the plugin already has a prefix
88+
if (pluginName.includes('/')) {
89+
return pluginName;
90+
}
91+
92+
return this.activePrefix + '/' + pluginName;
93+
}
94+
6495
handleRequest(method, URL, body, callback) {
6596
var self = this;
6697

@@ -109,11 +140,14 @@ export default class WpeApi {
109140

110141
// Compatibility method to deal with transitioning APIs and older version of Thunder
111142
// note: This assumes the WebSocket to jsonrpc will fail.
112-
req(rest, jsonrpc) {
143+
req(rest, jsonrpc, options = {}) {
113144
return new Promise( (resolve, reject) => {
114145
if (jsonrpc) {
115-
console.debug(`<JSON> ${jsonrpc.plugin }.1.${jsonrpc.method}`, jsonrpc.params ? jsonrpc.params : '');
116-
this.t.call(jsonrpc.plugin, jsonrpc.method, jsonrpc.params)
146+
// Apply active prefix to plugin name unless skipPrefix is set
147+
// skipPrefix is used for infrastructure calls with absolute paths
148+
const prefixedPlugin = options.skipPrefix ? jsonrpc.plugin : this.getPrefixedPlugin(jsonrpc.plugin);
149+
console.debug(`<JSON> ${prefixedPlugin}.1.${jsonrpc.method}`, jsonrpc.params ? jsonrpc.params : '');
150+
this.t.call(prefixedPlugin, jsonrpc.method, jsonrpc.params)
117151
.then( result => {
118152
resolve(result);
119153
}).catch( error => {

0 commit comments

Comments
 (0)