Compare commits
No commits in common. "2227cb60fe9907cc949accd9b7b5f62172c352ee" and "4fbc4e15943abaa1ce3ce621354af6b9708ac107" have entirely different histories.
2227cb60fe
...
4fbc4e1594
3 changed files with 75 additions and 99 deletions
|
|
@ -15,10 +15,7 @@ PluginComponent {
|
|||
property string tailscaleIP: ""
|
||||
property string currentExitNode: ""
|
||||
property var peers: []
|
||||
property string cachedClipboardTool: ""
|
||||
property string _copyText: ""
|
||||
property string _copyCurrentTool: ""
|
||||
property int _copyAttempted: 0
|
||||
property string clipboardCmd: ""
|
||||
|
||||
layerNamespacePlugin: "tailscalectl"
|
||||
popoutWidth: 360
|
||||
|
|
@ -32,19 +29,16 @@ PluginComponent {
|
|||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
statusCheck.running = true
|
||||
detectClipboard.running = true
|
||||
}
|
||||
|
||||
Process {
|
||||
id: toggleProcess
|
||||
|
||||
stderr: StdioCollector {}
|
||||
|
||||
onExited: (code, status) => {
|
||||
if (code !== 0) {
|
||||
var action = root.isConnected ? "disconnect" : "connect"
|
||||
var detail = toggleProcess.stderr.text.trim().slice(0, 120)
|
||||
ToastService.showError("tailscalectl", TailscaleLib.errorMessage(action) + (detail ? " — " + detail : ""))
|
||||
ToastService.showError("tailscalectl", TailscaleLib.errorMessage(action))
|
||||
}
|
||||
statusCheck.running = true
|
||||
}
|
||||
|
|
@ -52,40 +46,35 @@ PluginComponent {
|
|||
|
||||
Process {
|
||||
id: copyProcess
|
||||
|
||||
stderr: StdioCollector {}
|
||||
|
||||
onExited: (code, status) => {
|
||||
if (code === 0) {
|
||||
root.cachedClipboardTool = root._copyCurrentTool
|
||||
ToastService.showInfo("Copied " + root._copyText + " to clipboard")
|
||||
} else {
|
||||
root._copyAttempted += 1
|
||||
if (root._copyAttempted < TailscaleLib.allClipboardTools().length) {
|
||||
root._copyCurrentTool = TailscaleLib.nextClipboardTool(root._copyCurrentTool)
|
||||
root._executeCopy()
|
||||
} else {
|
||||
var detail = copyProcess.stderr.text.trim().slice(0, 120)
|
||||
ToastService.showError("tailscalectl", "No clipboard tool found" + (detail ? " — " + detail : ""))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: exitNodeProcess
|
||||
|
||||
stderr: StdioCollector {}
|
||||
|
||||
onExited: (code, status) => {
|
||||
if (code !== 0) {
|
||||
var detail = exitNodeProcess.stderr.text.trim().slice(0, 120)
|
||||
ToastService.showError("tailscalectl", TailscaleLib.errorMessage("set") + (detail ? " — " + detail : ""))
|
||||
ToastService.showError("tailscalectl", TailscaleLib.errorMessage("set"))
|
||||
}
|
||||
statusCheck.running = true
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: detectClipboard
|
||||
|
||||
command: ["sh", "-c", "if which dms >/dev/null 2>&1; then echo 'dms cl copy'; elif which wl-copy >/dev/null 2>&1; then echo 'wl-copy'; elif which clipmanctl >/dev/null 2>&1; then echo 'clipmanctl copy'; elif which xclip >/dev/null 2>&1; then echo 'xclip -selection clipboard'; elif which xsel >/dev/null 2>&1; then echo 'xsel --clipboard --input'; else echo none; fi"]
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
root.clipboardCmd = TailscaleLib.parseClipboardDetection(this.text)
|
||||
}
|
||||
}
|
||||
|
||||
onExited: (code, status) => {
|
||||
statusCheck.running = true
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: statusCheck
|
||||
|
||||
|
|
@ -136,24 +125,18 @@ PluginComponent {
|
|||
}
|
||||
|
||||
function copyToClipboard(text) {
|
||||
root._copyText = text
|
||||
root._copyAttempted = 0
|
||||
if (root.cachedClipboardTool) {
|
||||
root._copyCurrentTool = root.cachedClipboardTool
|
||||
} else {
|
||||
root._copyCurrentTool = TailscaleLib.allClipboardTools()[0]
|
||||
if (!root.clipboardCmd || root.clipboardCmd === "none") {
|
||||
ToastService.showError("tailscalectl", "No clipboard tool found")
|
||||
return
|
||||
}
|
||||
root._executeCopy()
|
||||
}
|
||||
|
||||
function _executeCopy() {
|
||||
var cmd = TailscaleLib.buildCopyCommand(root._copyText, root._copyCurrentTool)
|
||||
var cmd = TailscaleLib.buildCopyCommand(text, root.clipboardCmd)
|
||||
if (!cmd) {
|
||||
ToastService.showError("tailscalectl", "Invalid clipboard command")
|
||||
return
|
||||
}
|
||||
copyProcess.command = cmd
|
||||
copyProcess.running = true
|
||||
ToastService.showInfo("Copied " + text + " to clipboard")
|
||||
}
|
||||
|
||||
popoutContent: Component {
|
||||
|
|
|
|||
|
|
@ -26,34 +26,16 @@ function findActiveExitNode(peerMap) {
|
|||
return ""
|
||||
}
|
||||
|
||||
var safeClipboardTools = ["dms", "wl-copy", "clipmanctl"]
|
||||
var safeClipboardCmds = ["dms cl copy", "wl-copy", "clipmanctl copy", "xclip -selection clipboard", "xsel --clipboard --input", "none"]
|
||||
|
||||
var clipboardCmdMap = {
|
||||
"dms": "dms cl copy",
|
||||
"wl-copy": "wl-copy",
|
||||
"clipmanctl": "clipmanctl copy"
|
||||
function validateClipboardCmd(cmd) {
|
||||
return typeof cmd === "string" && safeClipboardCmds.includes(cmd)
|
||||
}
|
||||
|
||||
function validateClipboardTool(tool) {
|
||||
return typeof tool === "string" && safeClipboardTools.includes(tool)
|
||||
}
|
||||
|
||||
function buildCopyCommand(text, tool) {
|
||||
if (!validateClipboardTool(tool)) return null
|
||||
var cmd = clipboardCmdMap[tool]
|
||||
if (!cmd) return null
|
||||
function buildCopyCommand(text, clipboardCmd) {
|
||||
if (!validateClipboardCmd(clipboardCmd)) return null
|
||||
var escaped = text.replace(/'/g, "'\\''")
|
||||
return ["sh", "-c", "printf '%s' '" + escaped + "' | " + cmd]
|
||||
}
|
||||
|
||||
function nextClipboardTool(currentTool) {
|
||||
var idx = safeClipboardTools.indexOf(currentTool)
|
||||
if (idx < 0) return safeClipboardTools[0]
|
||||
return safeClipboardTools[(idx + 1) % safeClipboardTools.length]
|
||||
}
|
||||
|
||||
function allClipboardTools() {
|
||||
return safeClipboardTools.slice()
|
||||
return ["sh", "-c", "printf '%s' '" + escaped + "' | " + clipboardCmd]
|
||||
}
|
||||
|
||||
function parseStatusResult(jsonText) {
|
||||
|
|
@ -74,6 +56,11 @@ function buildToggleCommand(isConnected) {
|
|||
return isConnected ? ["tailscale", "down"] : ["tailscale", "up"]
|
||||
}
|
||||
|
||||
function parseClipboardDetection(stdout) {
|
||||
var trimmed = typeof stdout === "string" ? stdout.trim() : ""
|
||||
return validateClipboardCmd(trimmed) ? trimmed : "none"
|
||||
}
|
||||
|
||||
function errorMessage(cmd) {
|
||||
var messages = {
|
||||
"up": "Failed to connect to Tailscale",
|
||||
|
|
@ -88,5 +75,5 @@ function errorMessage(cmd) {
|
|||
|
||||
// CommonJS export for Node.js tests (ignored by QML)
|
||||
if (typeof module !== "undefined" && module.exports) {
|
||||
module.exports = { parsePeers, makeExitNodeCommand, findActiveExitNode, errorMessage, validateClipboardTool, buildCopyCommand, nextClipboardTool, allClipboardTools, buildToggleCommand, parseStatusResult }
|
||||
module.exports = { parsePeers, makeExitNodeCommand, findActiveExitNode, errorMessage, validateClipboardCmd, buildCopyCommand, parseClipboardDetection, buildToggleCommand, parseStatusResult }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { test } from "node:test"
|
||||
import assert from "node:assert"
|
||||
import lib from "../tailscalectl/lib.js"
|
||||
const { parsePeers, makeExitNodeCommand, findActiveExitNode, errorMessage, validateClipboardTool, buildCopyCommand, nextClipboardTool, allClipboardTools, buildToggleCommand, parseStatusResult } = lib
|
||||
const { parsePeers, makeExitNodeCommand, findActiveExitNode, errorMessage, validateClipboardCmd, buildCopyCommand, parseClipboardDetection, buildToggleCommand, parseStatusResult } = lib
|
||||
|
||||
test("parsePeers extracts exitNode from ExitNodeOption", () => {
|
||||
const peerMap = {
|
||||
|
|
@ -75,25 +75,28 @@ test("errorMessage returns generic message for unknown command", () => {
|
|||
assert.strictEqual(msg, "Tailscale command failed")
|
||||
})
|
||||
|
||||
// --- validateClipboardTool ---
|
||||
// --- validateClipboardCmd ---
|
||||
|
||||
test("validateClipboardTool accepts whitelisted tool names", () => {
|
||||
assert.strictEqual(validateClipboardTool("dms"), true)
|
||||
assert.strictEqual(validateClipboardTool("wl-copy"), true)
|
||||
assert.strictEqual(validateClipboardTool("clipmanctl"), true)
|
||||
test("validateClipboardCmd accepts whitelisted values", () => {
|
||||
assert.strictEqual(validateClipboardCmd("dms cl copy"), true)
|
||||
assert.strictEqual(validateClipboardCmd("wl-copy"), true)
|
||||
assert.strictEqual(validateClipboardCmd("clipmanctl copy"), true)
|
||||
assert.strictEqual(validateClipboardCmd("xclip -selection clipboard"), true)
|
||||
assert.strictEqual(validateClipboardCmd("xsel --clipboard --input"), true)
|
||||
assert.strictEqual(validateClipboardCmd("none"), true)
|
||||
})
|
||||
|
||||
test("validateClipboardTool rejects malicious inputs", () => {
|
||||
assert.strictEqual(validateClipboardTool("rm -rf /"), false)
|
||||
assert.strictEqual(validateClipboardTool("echo hi; malicious"), false)
|
||||
assert.strictEqual(validateClipboardTool("$(whoami)"), false)
|
||||
assert.strictEqual(validateClipboardTool("wl-copy || rm -rf /"), false)
|
||||
test("validateClipboardCmd rejects malicious inputs", () => {
|
||||
assert.strictEqual(validateClipboardCmd("rm -rf /"), false)
|
||||
assert.strictEqual(validateClipboardCmd("echo hi; malicious"), false)
|
||||
assert.strictEqual(validateClipboardCmd("$(whoami)"), false)
|
||||
assert.strictEqual(validateClipboardCmd("wl-copy || rm -rf /"), false)
|
||||
})
|
||||
|
||||
test("validateClipboardTool rejects empty and falsy inputs", () => {
|
||||
assert.strictEqual(validateClipboardTool(""), false)
|
||||
assert.strictEqual(validateClipboardTool(null), false)
|
||||
assert.strictEqual(validateClipboardTool(undefined), false)
|
||||
test("validateClipboardCmd rejects empty and falsy inputs", () => {
|
||||
assert.strictEqual(validateClipboardCmd(""), false)
|
||||
assert.strictEqual(validateClipboardCmd(null), false)
|
||||
assert.strictEqual(validateClipboardCmd(undefined), false)
|
||||
})
|
||||
|
||||
// --- buildCopyCommand ---
|
||||
|
|
@ -105,7 +108,7 @@ test("buildCopyCommand returns safe command for whitelisted clipboard tool", ()
|
|||
assert.strictEqual(cmd[1], "-c")
|
||||
})
|
||||
|
||||
test("buildCopyCommand returns null for invalid clipboard tool", () => {
|
||||
test("buildCopyCommand returns null for invalid clipboard command", () => {
|
||||
assert.strictEqual(buildCopyCommand("hello", "rm -rf /"), null)
|
||||
assert.strictEqual(buildCopyCommand("hello", ""), null)
|
||||
assert.strictEqual(buildCopyCommand("hello", null), null)
|
||||
|
|
@ -116,28 +119,31 @@ test("buildCopyCommand safely handles text with special characters", () => {
|
|||
assert.ok(Array.isArray(cmd))
|
||||
})
|
||||
|
||||
// --- nextClipboardTool ---
|
||||
// --- parseClipboardDetection ---
|
||||
|
||||
test("nextClipboardTool cycles through tools", () => {
|
||||
assert.strictEqual(nextClipboardTool("dms"), "wl-copy")
|
||||
assert.strictEqual(nextClipboardTool("wl-copy"), "clipmanctl")
|
||||
assert.strictEqual(nextClipboardTool("clipmanctl"), "dms")
|
||||
test("parseClipboardDetection passes through expected output strings", () => {
|
||||
assert.strictEqual(parseClipboardDetection("dms cl copy"), "dms cl copy")
|
||||
assert.strictEqual(parseClipboardDetection("wl-copy"), "wl-copy")
|
||||
assert.strictEqual(parseClipboardDetection("clipmanctl copy"), "clipmanctl copy")
|
||||
assert.strictEqual(parseClipboardDetection("none"), "none")
|
||||
})
|
||||
|
||||
test("nextClipboardTool falls back to first tool for unknown input", () => {
|
||||
assert.strictEqual(nextClipboardTool(""), "dms")
|
||||
assert.strictEqual(nextClipboardTool("unknown"), "dms")
|
||||
test("parseClipboardDetection trims extra whitespace", () => {
|
||||
assert.strictEqual(parseClipboardDetection(" wl-copy \n"), "wl-copy")
|
||||
})
|
||||
|
||||
// --- allClipboardTools ---
|
||||
test("parseClipboardDetection falls back to none for multi-line output", () => {
|
||||
assert.strictEqual(parseClipboardDetection("wl-copy\nrm -rf /"), "none")
|
||||
})
|
||||
|
||||
test("allClipboardTools returns the list of safe tools", () => {
|
||||
const tools = allClipboardTools()
|
||||
assert.ok(Array.isArray(tools))
|
||||
assert.strictEqual(tools.length, 3)
|
||||
assert.ok(tools.includes("dms"))
|
||||
assert.ok(tools.includes("wl-copy"))
|
||||
assert.ok(tools.includes("clipmanctl"))
|
||||
test("parseClipboardDetection falls back to empty output", () => {
|
||||
assert.strictEqual(parseClipboardDetection(""), "none")
|
||||
assert.strictEqual(parseClipboardDetection(" "), "none")
|
||||
})
|
||||
|
||||
test("parseClipboardDetection falls back to none for unexpected strings", () => {
|
||||
assert.strictEqual(parseClipboardDetection("pbpaste"), "none")
|
||||
assert.strictEqual(parseClipboardDetection("custom-tool"), "none")
|
||||
})
|
||||
|
||||
// --- buildToggleCommand ---
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue