Added support for multiple tabs in URL

This adds support for storing multiple tabs' contents and headers in the URL (with backwards compatibility for the current standard '&input=...') and moves the tab header information to the InputWorker instead of an array in the TabWaiter.
This commit is contained in:
Michael Rowley 2022-11-07 18:07:45 +00:00
parent b615d1350d
commit bbea58dabb
7 changed files with 206 additions and 167 deletions

View file

@ -212,29 +212,6 @@ class App {
this.manager.worker.silentBake(recipeConfig); this.manager.worker.silentBake(recipeConfig);
} }
/**
* Sets the user's input data.
*
* @param {string} input - The string to set the input to
*/
setInput(input) {
// Get the currently active tab.
// If there isn't one, assume there are no inputs so use inputNum of 1
let inputNum = this.manager.tabs.getActiveInputTab();
if (inputNum === -1) inputNum = 1;
this.manager.input.updateInputValue(inputNum, input);
this.manager.input.inputWorker.postMessage({
action: "setInput",
data: {
inputNum: inputNum,
silent: true
}
});
}
/** /**
* Populates the operations accordion list with the categories and operations specified in the * Populates the operations accordion list with the categories and operations specified in the
* view constructor. * view constructor.
@ -483,13 +460,37 @@ class App {
} }
// Read in input data from URI params // Read in input data from URI params
if (this.uriParams.input) { this.manager.input.clearAllIoClick();
try { let maxInputNum = 1;
const inputData = fromBase64(this.uriParams.input); for (const [param, value] of Object.entries(this.uriParams)) {
this.setInput(inputData); if (typeof param !== "string") {
} catch (err) {} continue;
} }
const inputHeaderRegex = /input\[('?[A-Za-z0-9\-_]+)'?\]/.exec(param);
if (inputHeaderRegex === null && param !== "input") {
// Invalid parameter key.
continue;
}
if (maxInputNum > 1) {
this.manager.input.addInput(false);
}
if (inputHeaderRegex !== null && inputHeaderRegex.length !== 1) {
// This input has a custom header that can be 'imported'.
const header = inputHeaderRegex[1];
if (header[0] === "'") {
this.manager.input.setTabName(maxInputNum, fromBase64(header.substring(1)));
}
}
this.manager.input.updateInputValue(maxInputNum, fromBase64(value));
maxInputNum++;
}
this.manager.input.changeTab(1, true);
this.manager.input.bakeAll();
// Read in theme from URI params // Read in theme from URI params
if (this.uriParams.theme) { if (this.uriParams.theme) {
this.manager.options.changeTheme(Utils.escapeHtml(this.uriParams.theme)); this.manager.options.changeTheme(Utils.escapeHtml(this.uriParams.theme));
@ -730,18 +731,16 @@ class App {
this.progress = 0; this.progress = 0;
this.autoBake(); this.autoBake();
this.updateTitle(true, null, true); this.updateTitle(true);
} }
/** /**
* Update the page title to contain the new recipe * Update the page title to contain the new recipe
* *
* @param {boolean} includeInput
* @param {string} input
* @param {boolean} [changeUrl=true] * @param {boolean} [changeUrl=true]
*/ */
updateTitle(includeInput, input, changeUrl=true) { updateTitle(changeUrl=true) {
// Set title // Set title
const recipeConfig = this.getRecipeConfig(); const recipeConfig = this.getRecipeConfig();
let title = "CyberChef"; let title = "CyberChef";
@ -761,8 +760,10 @@ class App {
// Update the current history state (not creating a new one) // Update the current history state (not creating a new one)
if (this.options.updateUrl && changeUrl) { if (this.options.updateUrl && changeUrl) {
this.lastStateUrl = this.manager.controls.generateStateUrl(true, includeInput, input, recipeConfig); this.manager.controls.generateStateUrl(true, true, recipeConfig).then((lastStateUrl) => {
this.lastStateUrl = lastStateUrl;
window.history.replaceState({}, title, this.lastStateUrl); window.history.replaceState({}, title, this.lastStateUrl);
});
} }
} }

View file

@ -586,6 +586,13 @@
Keep the current tab in sync between the input and output Keep the current tab in sync between the input and output
</label> </label>
</div> </div>
<div class="checkbox option-item">
<label for="emptyInputPreserve">
<input type="checkbox" option="emptyInputPreserve" id="emptyInputPreserve" checked>
Include empty inputs in URL
</label>
</div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary" id="reset-options">Reset options to default</button> <button type="button" class="btn btn-secondary" id="reset-options">Reset options to default</button>

View file

@ -54,7 +54,8 @@ function main() {
autoMagic: true, autoMagic: true,
imagePreview: true, imagePreview: true,
syncTabs: true, syncTabs: true,
preserveCR: "entropy" preserveCR: "entropy",
emptyInputPreserve: false
}; };
document.removeEventListener("DOMContentLoaded", main, false); document.removeEventListener("DOMContentLoaded", main, false);

View file

@ -5,6 +5,7 @@
*/ */
import Utils from "../../core/Utils.mjs"; import Utils from "../../core/Utils.mjs";
import { toBase64 } from "../../core/lib/Base64.mjs";
/** /**
@ -100,10 +101,10 @@ class ControlsWaiter {
const includeRecipe = document.getElementById("save-link-recipe-checkbox").checked; const includeRecipe = document.getElementById("save-link-recipe-checkbox").checked;
const includeInput = document.getElementById("save-link-input-checkbox").checked; const includeInput = document.getElementById("save-link-input-checkbox").checked;
const saveLinkEl = document.getElementById("save-link"); const saveLinkEl = document.getElementById("save-link");
const saveLink = this.generateStateUrl(includeRecipe, includeInput, null, recipeConfig); this.generateStateUrl(includeRecipe, includeInput, recipeConfig).then((saveLink) => {
saveLinkEl.innerHTML = Utils.escapeHtml(Utils.truncate(saveLink, 120)); saveLinkEl.innerHTML = Utils.escapeHtml(Utils.truncate(saveLink, 120));
saveLinkEl.setAttribute("href", saveLink); saveLinkEl.setAttribute("href", saveLink);
});
} }
@ -112,12 +113,11 @@ class ControlsWaiter {
* *
* @param {boolean} includeRecipe - Whether to include the recipe in the URL. * @param {boolean} includeRecipe - Whether to include the recipe in the URL.
* @param {boolean} includeInput - Whether to include the input in the URL. * @param {boolean} includeInput - Whether to include the input in the URL.
* @param {string} input
* @param {Object[]} [recipeConfig] - The recipe configuration object array. * @param {Object[]} [recipeConfig] - The recipe configuration object array.
* @param {string} [baseURL] - The CyberChef URL, set to the current URL if not included * @param {string} [baseURL] - The CyberChef URL, set to the current URL if not included
* @returns {string} * @returns {string}
*/ */
generateStateUrl(includeRecipe, includeInput, input, recipeConfig, baseURL) { async generateStateUrl(includeRecipe, includeInput, recipeConfig, baseURL) {
recipeConfig = recipeConfig || this.app.getRecipeConfig(); recipeConfig = recipeConfig || this.app.getRecipeConfig();
const link = baseURL || window.location.protocol + "//" + const link = baseURL || window.location.protocol + "//" +
@ -127,21 +127,30 @@ class ControlsWaiter {
includeRecipe = includeRecipe && (recipeConfig.length > 0); includeRecipe = includeRecipe && (recipeConfig.length > 0);
// If we don't get passed an input, get it from the current URI const params = [includeRecipe ? ["recipe", recipeStr] : undefined];
if (input === null && includeInput) {
const params = this.app.getURIParams(); if (includeInput) {
if (params.input) { // getTabList() only returns visible tabs so we need to use getInputNums().
includeInput = true; const inputRange = (await this.manager.input.getInputNums()).inputNums;
input = params.input; for (let i = 0; i < inputRange.length; i++) {
} else { const inputNum = parseInt(inputRange[i], 10);
includeInput = false;
} const inputValue = toBase64(await this.manager.input.getInputValue(inputNum), "A-Za-z0-9-_");
if (typeof inputValue !== "string" || inputValue.length > 2048 ||
(!this.app.options.emptyInputPreserve && inputValue.length === 0)) {
// Don't store other datatypes or strings that are too long (arbitrary size limit).
continue;
} }
const params = [ let inputIdentifier = inputNum.toString();
includeRecipe ? ["recipe", recipeStr] : undefined, const tabHeader = await this.manager.input.getTabName(inputNum);
includeInput ? ["input", Utils.escapeHtml(input)] : undefined, if (typeof tabHeader === "string" && tabHeader.length > 0) {
]; inputIdentifier = "'" + toBase64(tabHeader, "A-Za-z0-9-_") + "'";
}
params.push([`input[${inputIdentifier}]`, inputValue]);
}
}
const hash = params const hash = params
.filter(v => v) .filter(v => v)
@ -344,8 +353,7 @@ class ControlsWaiter {
e.preventDefault(); e.preventDefault();
const reportBugInfo = document.getElementById("report-bug-info"); const reportBugInfo = document.getElementById("report-bug-info");
const saveLink = this.generateStateUrl(true, true, null, null, "https://gchq.github.io/CyberChef/"); this.generateStateUrl(true, true, null, "https://gchq.github.io/CyberChef/").then((saveLink) => {
if (reportBugInfo) { if (reportBugInfo) {
reportBugInfo.innerHTML = `* Version: ${PKG_VERSION} reportBugInfo.innerHTML = `* Version: ${PKG_VERSION}
* Compile time: ${COMPILE_TIME} * Compile time: ${COMPILE_TIME}
@ -355,6 +363,7 @@ ${navigator.userAgent}
`; `;
} }
});
} }

View file

@ -8,7 +8,6 @@
import LoaderWorker from "worker-loader?inline=no-fallback!../workers/LoaderWorker.js"; import LoaderWorker from "worker-loader?inline=no-fallback!../workers/LoaderWorker.js";
import InputWorker from "worker-loader?inline=no-fallback!../workers/InputWorker.mjs"; import InputWorker from "worker-loader?inline=no-fallback!../workers/InputWorker.mjs";
import Utils, { debounce } from "../../core/Utils.mjs"; import Utils, { debounce } from "../../core/Utils.mjs";
import { toBase64 } from "../../core/lib/Base64.mjs";
import { isImage } from "../../core/lib/FileType.mjs"; import { isImage } from "../../core/lib/FileType.mjs";
@ -291,7 +290,7 @@ class InputWaiter {
this.app.handleError(r.data); this.app.handleError(r.data);
break; break;
case "setUrl": case "setUrl":
this.setUrl(r.data); this.setUrl();
break; break;
case "inputSwitch": case "inputSwitch":
this.manager.output.inputSwitch(r.data); this.manager.output.inputSwitch(r.data);
@ -306,6 +305,9 @@ class InputWaiter {
case "fileLoaded": case "fileLoaded":
this.fileLoaded(r.data.inputNum); this.fileLoaded(r.data.inputNum);
break; break;
case "getTabName":
this.callbacks[r.data.id](r.data);
break;
default: default:
log.error(`Unknown action ${r.action}.`); log.error(`Unknown action ${r.action}.`);
} }
@ -363,14 +365,7 @@ class InputWaiter {
inputData.input.count("\n") + 1 : null; inputData.input.count("\n") + 1 : null;
this.setInputInfo(inputData.input.length, lines); this.setInputInfo(inputData.input.length, lines);
// Set URL to current input this.setUrl();
const inputStr = toBase64(inputData.input, "A-Za-z0-9+/");
if (inputStr.length > 0 && inputStr.length <= 68267) {
this.setUrl({
includeInput: true,
input: inputStr
});
}
if (!silent) window.dispatchEvent(this.manager.statechange); if (!silent) window.dispatchEvent(this.manager.statechange);
} else { } else {
@ -527,15 +522,7 @@ class InputWaiter {
* @param {boolean} [force=false] - If true, forces the value to be updated even if the type is different to the currently stored type * @param {boolean} [force=false] - If true, forces the value to be updated even if the type is different to the currently stored type
*/ */
updateInputValue(inputNum, value, force=false) { updateInputValue(inputNum, value, force=false) {
let includeInput = false; this.setUrl();
const recipeStr = toBase64(value, "A-Za-z0-9+/"); // B64 alphabet with no padding
if (recipeStr.length > 0 && recipeStr.length <= 68267) {
includeInput = true;
}
this.setUrl({
includeInput: includeInput,
input: recipeStr
});
// Value is either a string set by the input or an ArrayBuffer from a LoaderWorker, // Value is either a string set by the input or an ArrayBuffer from a LoaderWorker,
// so is safe to use typeof === "string" // so is safe to use typeof === "string"
@ -1084,6 +1071,7 @@ class InputWaiter {
this.setupInputWorker(); this.setupInputWorker();
this.manager.worker.setupChefWorker(); this.manager.worker.setupChefWorker();
this.addInput(true); this.addInput(true);
this.manager.input.changeTab(1, true);
this.bakeAll(); this.bakeAll();
} }
@ -1095,7 +1083,7 @@ class InputWaiter {
const inputNum = this.manager.tabs.getActiveInputTab(); const inputNum = this.manager.tabs.getActiveInputTab();
if (inputNum === -1) return; if (inputNum === -1) return;
this.manager.tabs.removeTabHeaderAlias(inputNum); this.manager.input.setTabName(inputNum, "");
this.manager.highlighter.removeHighlights(); this.manager.highlighter.removeHighlights();
getSelection().removeAllRanges(); getSelection().removeAllRanges();
@ -1234,7 +1222,6 @@ class InputWaiter {
removeChefWorker: true removeChefWorker: true
} }
}); });
this.manager.tabs.removeTabHeaderAlias(inputNum);
this.manager.output.removeTab(inputNum); this.manager.output.removeTab(inputNum);
} }
@ -1248,10 +1235,9 @@ class InputWaiter {
if (!mouseEvent.target) { if (!mouseEvent.target) {
return; return;
} }
const tabNumStr = mouseEvent.target.closest("button").parentElement.getAttribute("inputNum"); const tabNum = mouseEvent.target.closest("button").parentElement.getAttribute("inputNum");
if (tabNumStr) { if (tabNum) {
const tabNum = parseInt(tabNumStr, 10); this.removeInput(parseInt(tabNum, 10));
this.removeInput(tabNum);
} }
} }
@ -1437,13 +1423,61 @@ class InputWaiter {
/** /**
* Update the input URL to the new value * Update the input URL to the new value
*
* @param {object} urlData - Object containing the URL data
* @param {boolean} urlData.includeInput - If true, the input is included in the title
* @param {string} urlData.input - The input data to be included
*/ */
setUrl(urlData) { setUrl() {
this.app.updateTitle(urlData.includeInput, urlData.input, true); this.app.updateTitle(true);
}
/**
* Retrieves the custom name of any tab
*
* @param {number} inputNum - The input number of the tab
* @returns {string} - The tab's custom name or null if it has not been assigned one
*/
async getTabName(inputNum) {
if (inputNum <= 0) {
return null;
}
const tabName = (await new Promise(resolve => {
this.queryTabName(r => {
resolve(r);
}, inputNum);
})).data;
return tabName === null ? "" : tabName;
}
/**
* Sends the inputWorker a message requesting the custom name of a given tab
*
* @param {object} callback - The callback to be executed after the worker finishes
* @param {number} inputNum - The input number of the tab
*/
queryTabName(callback, inputNum) {
const callbackId = this.callbackID++;
this.callbacks[callbackId] = callback;
this.inputWorker.postMessage({
action: "getTabName",
data: {
id: callbackId,
inputNum: inputNum
}
});
}
/**
* Assigns the provided tab with a custo name
*
* @param {number} inputNum - The input number of the tab
* @param {string} tabName - The new name for the tab
*/
setTabName(inputNum, tabName) {
this.inputWorker.postMessage({
action: "setTabName",
data: {
inputNum: inputNum,
tabName: tabName
}
});
} }
/** /**
@ -1461,9 +1495,6 @@ class InputWaiter {
let renameContents = targetElement.textContent; let renameContents = targetElement.textContent;
const renameContentsColon = renameContents.indexOf(":"); const renameContentsColon = renameContents.indexOf(":");
// Calling 'getInputValue()' might take a long time for large datasets,
// it could be beneficial to modify the API to allow for querying whether
// there is any input rather than getting the full string just to access its length.
const inputLength = (await this.getInputValue(this.manager.tabs.getActiveInputTab())).length; const inputLength = (await this.getInputValue(this.manager.tabs.getActiveInputTab())).length;
// Remove the data from the renaming section // Remove the data from the renaming section
@ -1499,20 +1530,25 @@ class InputWaiter {
if (activeInputTabNum === -1) { if (activeInputTabNum === -1) {
return; return;
} }
const activeInputTabElement = this.manager.tabs.getTabItem(activeInputTabNum, "input"); const activeInputTabElement = this.manager.tabs.getTabItem(activeInputTabNum, "input");
if (activeInputTabElement == null || activeInputTabElement.children.size < 1) { if (activeInputTabElement == null || activeInputTabElement.children.size < 1) {
return; return;
} }
const tabContent = activeInputTabElement.children[0]; const tabContent = activeInputTabElement.children[0];
const tabHeader = tabContent.children[0].children[0].value; const tabHeader = tabContent.children[0].children[0].value;
const inputContents = await this.getInputValue(activeInputTabNum); const inputContents = await this.getInputValue(activeInputTabNum);
if (tabHeader.length === 0) if (tabHeader.length === 0)
this.manager.tabs.removeTabHeaderAlias(activeInputTabNum, false); this.manager.input.setTabName(activeInputTabNum, "");
else else
this.manager.tabs.addTabHeaderAlias(activeInputTabNum, tabHeader); this.manager.input.setTabName(activeInputTabNum, tabHeader);
this.manager.tabs.updateInputTabHeader(activeInputTabNum, inputContents); this.manager.tabs.updateInputTabHeader(activeInputTabNum, inputContents);
this.manager.tabs.updateOutputTabHeader(activeInputTabNum, inputContents); this.manager.tabs.updateOutputTabHeader(activeInputTabNum, inputContents);
this.manager.input.setUrl();
} }
} }

View file

@ -18,7 +18,6 @@ class TabWaiter {
constructor(app, manager) { constructor(app, manager) {
this.app = app; this.app = app;
this.manager = manager; this.manager = manager;
this.tabHeaderAliases = []; // Mapping custom tab headers to indexes/numbers.
} }
/** /**
@ -151,6 +150,8 @@ class TabWaiter {
const newTab = document.createElement("li"); const newTab = document.createElement("li");
newTab.setAttribute("inputNum", inputNum.toString()); newTab.setAttribute("inputNum", inputNum.toString());
newTab.setAttribute("tabHeader", "");
if (active) newTab.classList.add(`active-${io}-tab`); if (active) newTab.classList.add(`active-${io}-tab`);
const newTabContent = document.createElement("div"); const newTabContent = document.createElement("div");
@ -178,6 +179,8 @@ class TabWaiter {
newTab.appendChild(newTabButton); newTab.appendChild(newTabButton);
} }
this.manager.input.setUrl();
return newTab; return newTab;
} }
@ -353,11 +356,9 @@ class TabWaiter {
const tab = this.getTabItem(inputNum, io); const tab = this.getTabItem(inputNum, io);
if (tab == null) return; if (tab == null) return;
const customHeaderData = this.getTabHeaderAlias(inputNum); const customHeaderData = await this.manager.input.getTabName(inputNum);
// When 'customHeaderData === `Tab ${inputNum}`' is true, it's usually due to const isStandardHeader = customHeaderData.length === 0 || customHeaderData === `Tab ${inputNum}`;
// a user having opened the rename textbox but then closing it without change.
const isStandardHeader = customHeaderData === null || customHeaderData === `Tab ${inputNum}`;
let headerData = isStandardHeader ? `Tab ${inputNum}` : customHeaderData; let headerData = isStandardHeader ? `Tab ${inputNum}` : customHeaderData;
const dataIsFile = data instanceof ArrayBuffer; const dataIsFile = data instanceof ArrayBuffer;
@ -375,10 +376,12 @@ class TabWaiter {
headerData += `: ${dataPreview}`; headerData += `: ${dataPreview}`;
} }
tab.firstElementChild.innerText = headerData; tab.firstElementChild.innerText = headerData;
if (!isStandardHeader && !includeData) if (!isStandardHeader && !includeData) {
tab.firstElementChild.innerText = `'${headerData}'`; tab.firstElementChild.innerText = `'${headerData}'`;
} }
}
/** /**
* Updates the input tab header to display a preview of the tab contents * Updates the input tab header to display a preview of the tab contents
@ -441,63 +444,6 @@ class TabWaiter {
updateOutputTabProgress(inputNum, progress, total) { updateOutputTabProgress(inputNum, progress, total) {
this.updateTabProgress(inputNum, progress, total, "output"); this.updateTabProgress(inputNum, progress, total, "output");
} }
/**
* Adds an alias between a custom tab header and a tab number so that
* mapping between the two is possible if DOM element is removed.
*
* @param {number} tabNum - The index of the tab being aliased
* @param {string} tabHeader - The custom tab header
*/
addTabHeaderAlias(tabNum, tabHeader) {
// First, we try to overwrite an existing alias.
for (let i = 0; i < this.tabHeaderAliases.length; i++) {
if (this.tabHeaderAliases.at(i).tabNumber === tabNum) {
this.tabHeaderAliases.at(i).customHeader = tabHeader;
return;
}
}
this.tabHeaderAliases.push({tabNumber: tabNum, customHeader: tabHeader});
}
/**
* Removes a previously-assigned header alias.
*
* @param {number} tabNum - The index of the tab that should be removed.
* @param {boolean} shouldThrow - A boolean representing whether the function should throw an exception or return silently if it cannot locate the tab header.
*/
removeTabHeaderAlias(tabNum, shouldThrow) {
for (let i = 0; i < this.tabHeaderAliases.length; i++) {
if (this.tabHeaderAliases.at(i).tabNumber === tabNum) {
this.tabHeaderAliases.splice(i, 1);
return;
}
}
if (shouldThrow)
throw `Unable to locate header alias at tab index ${tabNum.toString()}.`;
}
/**
* Retrieves the custom header for a given tab.
*
* @param {number} tabNum - The index of the tab whose alias should be retrieved.
* @param {boolean} shouldThrow - Whether the function should throw an exception (instead of returning null) in the event of it being unable to locate the tab.
* @returns {string} customHeader - The custom header for the requested tab.
*/
getTabHeaderAlias(tabNum, shouldThrow) {
for (let i = 0; i < this.tabHeaderAliases.length; i++) {
if (this.tabHeaderAliases.at(i).tabNumber === tabNum)
return this.tabHeaderAliases.at(i).customHeader;
}
if (shouldThrow)
throw `Unable to locate header alias at tab index ${tabNum.toString()}.`;
return null;
}
} }
export default TabWaiter; export default TabWaiter;

View file

@ -113,6 +113,12 @@ self.addEventListener("message", function(e) {
case "getInputNums": case "getInputNums":
self.getInputNums(r.data); self.getInputNums(r.data);
break; break;
case "getTabName":
self.getTabName(r.data);
break;
case "setTabName":
self.setTabName(r.data.inputNum, r.data.tabName);
break;
default: default:
log.error(`Unknown action '${r.action}'.`); log.error(`Unknown action '${r.action}'.`);
} }
@ -413,14 +419,17 @@ self.getNearbyNums = function(inputNum, direction) {
}; };
/** /**
* Gets the data to display in the tab header for an input, and * Gets the data to display in the tab header for an input and
* posts it back to the inputWaiter * posts it back to the inputWaiter.
* *
* @param {number} inputNum - The inputNum of the tab header * @param {number} inputNum - The inputNum of the tab header.
*/ */
self.updateTabHeader = function(inputNum) { self.updateTabHeader = function(inputNum) {
const input = self.getInputObj(inputNum); const input = self.getInputObj(inputNum);
if (input === null || input === undefined) return; if (input === null || input === undefined) {
return;
}
let inputData = input.data; let inputData = input.data;
if (typeof inputData !== "string") { if (typeof inputData !== "string") {
inputData = input.data.name; inputData = input.data.name;
@ -1079,3 +1088,33 @@ self.inputSwitch = function(switchData) {
}); });
}; };
/**
* Gets the custom name of a tab, returning null if one has not been set
*
* @param {object} inputData
* @param {number} inputData.inputNum - The input number of the tab
* @param {number} inputData.id - The callback ID for the callvack to run when returning to the inputWaiter
*/
self.getTabName = function(inputData) {
const { inputNum, id } = inputData;
const inputName = self.inputs[inputNum].inputName;
const tabName = typeof inputName !== "string" ? null : inputName;
self.postMessage({
action: "getTabName",
data: {
data: tabName,
id: id
}
});
};
/**
* Sets the custom name of a tab, overwriting any previously-assigned one
*
* @param {number} inputNum - The input number of the tab
* @param {string} tabName - The custom name that should be assigned to the tab
*/
self.setTabName = function(inputNum, tabName) {
self.inputs[inputNum].inputName = tabName;
};