Skip to content

Commit a65e494

Browse files
committed
feat(PiConnect): Warning on token overwrite + fix token positional param
Signed-off-by: paulober <[email protected]>
1 parent 6c0429d commit a65e494

File tree

4 files changed

+178
-37
lines changed

4 files changed

+178
-37
lines changed

src/imagewriter.cpp

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@
4444
#include <QWindow>
4545
#include <QGuiApplication>
4646
#endif
47+
#include <QUrl>
48+
#include <QUrlQuery>
49+
#include <QString>
50+
#include <QStringList>
4751
#include <QHostAddress>
4852
#include <QNetworkAccessManager>
4953
#include <QNetworkReply>
@@ -2547,15 +2551,53 @@ void ImageWriter::openUrl(const QUrl &url)
25472551
}
25482552
}
25492553

2554+
static QString parseTokenFromUrl(const QUrl &url) {
2555+
// Handle QUrl or string, accept token/code/deploy_key/auth_key
2556+
if (!url.isValid()) {
2557+
return {};
2558+
}
2559+
2560+
QUrlQuery q(url);
2561+
static const QStringList keys {
2562+
QStringLiteral("token"),
2563+
QStringLiteral("code"),
2564+
QStringLiteral("deploy_key"),
2565+
QStringLiteral("auth_key")
2566+
};
2567+
2568+
for (const auto& key : keys) {
2569+
const QString val = q.queryItemValue(key, QUrl::FullyDecoded);
2570+
if (!val.isEmpty())
2571+
return val;
2572+
}
2573+
2574+
return {};
2575+
}
2576+
25502577
void ImageWriter::handleIncomingUrl(const QUrl &url)
25512578
{
25522579
qDebug() << "Incoming URL:" << url;
2553-
emit connectCallbackReceived(QVariant::fromValue(url));
2580+
2581+
auto token = parseTokenFromUrl(url);
2582+
if (!token.isEmpty()) {
2583+
if (!_piConnectToken.isEmpty()) {
2584+
if (_piConnectToken != token) {
2585+
// Let QML decide whether to overwrite
2586+
emit connectTokenConflictDetected(token);
2587+
}
2588+
2589+
return;
2590+
}
2591+
2592+
overwriteConnectToken(token);
2593+
}
25542594
}
25552595

2556-
void ImageWriter::setRuntimeConnectToken(const QString &token)
2596+
void ImageWriter::overwriteConnectToken(const QString &token)
25572597
{
2598+
// Ephemeral session-only Connect token (never persisted)
25582599
_piConnectToken = token;
2600+
emit connectTokenReceived(token);
25592601
}
25602602

25612603
QString ImageWriter::getRuntimeConnectToken() const

src/imagewriter.h

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -267,8 +267,7 @@ class ImageWriter : public QObject
267267
Q_INVOKABLE void reboot();
268268
Q_INVOKABLE void openUrl(const QUrl &url);
269269
Q_INVOKABLE void handleIncomingUrl(const QUrl &url);
270-
// Ephemeral session-only Connect token (never persisted)
271-
Q_INVOKABLE void setRuntimeConnectToken(const QString &token);
270+
Q_INVOKABLE void overwriteConnectToken(const QString &token);
272271
Q_INVOKABLE QString getRuntimeConnectToken() const;
273272

274273
/* Override OS list refresh schedule (in minutes); pass negative to clear override */
@@ -299,7 +298,8 @@ class ImageWriter : public QObject
299298
void keychainPermissionRequested();
300299
void keychainPermissionResponseReceived();
301300
void writeStateChanged();
302-
void connectCallbackReceived(QVariant url);
301+
void connectTokenReceived(const QString &token);
302+
void connectTokenConflictDetected(const QString &token);
303303
void cacheStatusChanged();
304304

305305
protected slots:

src/wizard/PiConnectCustomizationStep.qml

Lines changed: 17 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -84,49 +84,34 @@ WizardStepBase {
8484
property bool connectTokenReceived: false
8585
property string connectToken: ""
8686

87-
function parseTokenFromUrl(u) {
88-
// Handle QUrl or string, accept token/code/deploy_key/auth_key
89-
var s = ""
90-
try {
91-
if (u && typeof u.toString === 'function') s = u.toString(); else s = String(u)
92-
} catch(e) { s = String(u) }
93-
if (!s || s.length === 0) return ""
94-
var qIndex = s.indexOf('?')
95-
if (qIndex === -1) return ""
96-
var query = s.substring(qIndex+1)
97-
var parts = query.split('&')
98-
var token = ""
99-
for (var i = 0; i < parts.length; i++) {
100-
var kv = parts[i].split('=')
101-
if (kv.length >= 2) {
102-
var key = decodeURIComponent(kv[0])
103-
var val = decodeURIComponent(kv.slice(1).join('='))
104-
if (key === 'token' || key === 'code' || key === 'deploy_key' || key === 'auth_key') {
105-
token = val
106-
break
107-
}
87+
Connections {
88+
target: root.imageWriter
89+
90+
// Listen for callback with token
91+
function connectTokenReceived(token){
92+
if (token && token.length > 0) {
93+
connectTokenReceived = true
94+
connectToken = token
10895
}
10996
}
110-
return token
11197
}
11298

11399
Component.onCompleted: {
114100
root.registerFocusGroup("pi_connect", function(){ return [btnOpenConnect, useTokenPill.focusItem] }, 0)
101+
102+
var token = root.imageWriter.getRuntimeConnectToken()
103+
if (token && token.length > 0) {
104+
connectTokenReceived = true
105+
connectToken = token
106+
}
107+
115108
var saved = imageWriter.getSavedCustomizationSettings()
116109
// Never load token from persistent settings; token is session-only
117-
if (saved.piConnectEnabled === true || saved.piConnectEnabled === "true") {
110+
// auto enable if token has already been provided
111+
if (saved.piConnectEnabled === true || saved.piConnectEnabled === "true" || connectTokenReceived) {
118112
useTokenPill.checked = true
119113
wizardContainer.piConnectEnabled = true
120114
}
121-
// Listen for callback with token
122-
root.imageWriter.connectCallbackReceived.connect(function(url){
123-
var t = parseTokenFromUrl(url)
124-
if (t && t.length > 0) {
125-
connectTokenReceived = true
126-
connectToken = t
127-
imageWriter.setRuntimeConnectToken(t)
128-
}
129-
})
130115
}
131116

132117
onNextClicked: {

src/wizard/WizardContainer.qml

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -944,6 +944,120 @@ Item {
944944
onNextClicked: root.wizardCompleted()
945945
}
946946
}
947+
948+
// Token conflict dialog — based on your BaseDialog pattern
949+
BaseDialog {
950+
id: tokenConflictDialog
951+
parent: root
952+
anchors.centerIn: parent
953+
954+
// carry the new token we just received
955+
property string newToken: ""
956+
property bool allowAccept: false
957+
958+
// small safety delay before enabling "Replace"
959+
Timer {
960+
id: acceptEnableDelay
961+
interval: 1500
962+
running: false
963+
repeat: false
964+
onTriggered: tokenConflictDialog.allowAccept = true
965+
}
966+
967+
function openWithToken(tok) {
968+
newToken = tok
969+
allowAccept = false
970+
acceptEnableDelay.start()
971+
tokenConflictDialog.open()
972+
}
973+
974+
// ESC closes
975+
function escapePressed() { tokenConflictDialog.close() }
976+
977+
Component.onCompleted: {
978+
// match your focus group style
979+
registerFocusGroup("token_conflict_buttons", function() {
980+
return [keepBtn, replaceBtn]
981+
}, 0)
982+
}
983+
984+
onClosed: {
985+
acceptEnableDelay.stop()
986+
allowAccept = false
987+
newToken = ""
988+
}
989+
990+
// ----- CONTENT -----
991+
Text {
992+
id: titleText
993+
text: qsTr("Replace existing Raspberry Pi Connect token?")
994+
font.pixelSize: Style.fontSizeHeading
995+
font.family: Style.fontFamilyBold
996+
font.bold: true
997+
color: Style.formLabelColor
998+
wrapMode: Text.WordWrap
999+
Layout.fillWidth: true
1000+
Accessible.role: Accessible.Heading
1001+
Accessible.name: text
1002+
Accessible.ignored: false
1003+
}
1004+
1005+
// Body / security note
1006+
Text {
1007+
id: bodyText
1008+
text: qsTr("A new Raspberry Pi Connect token was received that differs from your current one.\n\n") +
1009+
qsTr("Do you want to overwrite the existing token?\n\n") +
1010+
qsTr("Warning: Only replace the token if you initiated this action. ") +
1011+
qsTr("If you didn't, someone could be trying to push a bad token to RPi Imager.")
1012+
font.pixelSize: Style.fontSizeFormLabel
1013+
font.family: Style.fontFamily
1014+
color: Style.formLabelColor
1015+
wrapMode: Text.WordWrap
1016+
Layout.fillWidth: true
1017+
Accessible.role: Accessible.StaticText
1018+
Accessible.name: text
1019+
Accessible.ignored: false
1020+
}
1021+
1022+
// Buttons row
1023+
RowLayout {
1024+
id: btnRow
1025+
Layout.fillWidth: true
1026+
Layout.topMargin: Style.spacingSmall
1027+
spacing: Style.spacingMedium
1028+
1029+
Item { Layout.fillWidth: true }
1030+
1031+
ImButton {
1032+
id: replaceBtn
1033+
text: tokenConflictDialog.allowAccept ? qsTr("Replace token") : qsTr("Please wait…")
1034+
accessibleDescription: qsTr("Replace the current token with the newly received one")
1035+
enabled: tokenConflictDialog.allowAccept
1036+
activeFocusOnTab: true
1037+
onClicked: {
1038+
tokenConflictDialog.close()
1039+
// Overwrite in C++ and re-emit to existing listeners
1040+
root.imageWriter.overwriteConnectToken(tokenConflictDialog.newToken)
1041+
}
1042+
}
1043+
1044+
ImButtonRed {
1045+
id: keepBtn
1046+
text: qsTr("Keep existing")
1047+
accessibleDescription: qsTr("Keep your current Raspberry Pi Connect token")
1048+
activeFocusOnTab: true
1049+
onClicked: tokenConflictDialog.close()
1050+
}
1051+
}
1052+
}
1053+
1054+
Connections {
1055+
target: root.imageWriter
1056+
function onConnectTokenConflictDetected(newToken) {
1057+
tokenConflictDialog.openWithToken(newToken)
1058+
}
1059+
}
1060+
9471061

9481062
function onFinalizing() {
9491063
// Forward to the WritingStep if currently active

0 commit comments

Comments
 (0)