feat(sync): le QR porte un lien pmserver://sync ouvert directement par l'app

Le scanner du telephone envoyait le texte a une recherche web (Google,
Chrome) : le jeton serait parti a un appui pres. Le lien a schema propre
n'a qu'une application pour l'ouvrir. Rien n'est enregistre avant Save.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
r-zakarya
2026-09-17 18:29:37 +01:00
parent 9bee179d1b
commit 780eecc11b
4 changed files with 79 additions and 32 deletions
+8
View File
@@ -93,6 +93,14 @@
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
<!-- Lien pmserver://sync?u=&t= du QR affiche par le poste : le
scanner du telephone l'ouvre directement dans l'app. -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="pmserver" android:host="sync" />
</intent-filter>
</activity> </activity>
<!-- FMX n'affiche pas BiometricPrompt depuis notre NativeActivity : il <!-- FMX n'affiche pas BiometricPrompt depuis notre NativeActivity : il
lance SA PROPRE activite, qui heberge le fragment androidx. Elle lance SA PROPRE activite, qui heberge le fragment androidx. Elle
+6 -8
View File
@@ -1336,8 +1336,10 @@ begin
// ne transite pas en clair par le moteur de rendu (meme raison que // ne transite pas en clair par le moteur de rendu (meme raison que
// prefs/has). Le mot de passe de synchro n'y est JAMAIS : un QR se // prefs/has). Le mot de passe de synchro n'y est JAMAIS : un QR se
// photographie par-dessus l'epaule. // photographie par-dessus l'epaule.
// JSON nu, sans prefixe "pmsync:" : un prefixe a deux-points se lit comme un // UN LIEN pmserver://sync?u=&t= ET NON DU TEXTE. Le scanner du telephone
// lien, et l'appareil photo proposait "Ouvrir avec" au lieu de copier. // envoie tout texte a une recherche web (Google, Chrome) - le jeton serait
// parti chez Google a un appui pres. Un lien a schema propre n'a qu'une
// application pour l'ouvrir : la notre.
else if ACmd = 'sync/qr' then else if ACmd = 'sync/qr' then
begin begin
var LQrUrl := PM.UserPrefs.GetPref('syncUrl'); var LQrUrl := PM.UserPrefs.GetPref('syncUrl');
@@ -1346,16 +1348,13 @@ begin
var LSize := 0; var LSize := 0;
if (LQrUrl <> '') and (LQrTok <> '') then if (LQrUrl <> '') and (LQrTok <> '') then
begin begin
var LJson := TJSONObject.Create;
var LQr := TDelphiZXingQRCode.Create; var LQr := TDelphiZXingQRCode.Create;
var LSb := TStringBuilder.Create; var LSb := TStringBuilder.Create;
try try
LJson.AddPair('pmsync', TJSONNumber.Create(1));
LJson.AddPair('u', LQrUrl);
LJson.AddPair('t', LQrTok);
LQr.Encoding := qrUTF8NoBOM; LQr.Encoding := qrUTF8NoBOM;
LQr.QuietZone := 4; LQr.QuietZone := 4;
LQr.Data := LJson.ToJSON; LQr.Data := 'pmserver://sync?u=' + TNetEncoding.URL.Encode(LQrUrl) +
'&t=' + TNetEncoding.URL.Encode(LQrTok);
LSize := LQr.Rows; LSize := LQr.Rows;
for var R := 0 to LQr.Rows - 1 do for var R := 0 to LQr.Rows - 1 do
for var C := 0 to LQr.Columns - 1 do for var C := 0 to LQr.Columns - 1 do
@@ -1364,7 +1363,6 @@ begin
finally finally
LSb.Free; LSb.Free;
LQr.Free; LQr.Free;
LJson.Free;
end; end;
end; end;
if LSize > 0 then if LSize > 0 then
+3 -3
View File
@@ -65,9 +65,9 @@ Bridge.onSyncQr = function (size, bits) {
'background:#fff;display:block;border-radius:6px">' + 'background:#fff;display:block;border-radius:6px">' +
'<path d="' + d + '" fill="#000"/></svg>' + '<path d="' + d + '" fill="#000"/></svg>' +
'<p style="font-size:11px;color:var(--text-faint);margin:6px 0 0">' + '<p style="font-size:11px;color:var(--text-faint);margin:6px 0 0">' +
'Scan it with the phone camera and copy the text, then in the app: ' + 'Scan it with the phone camera and open it with Coffre: the URL and the ' +
'Settings &rarr; Paste settings. It holds the URL and the token, not the ' + 'token fill in, then type the sync password (never in the QR) and Save. ' +
'sync password. Hidden again in 60 s.</p>'; 'Hidden again in 60 s.</p>';
box.hidden = false; box.hidden = false;
clearTimeout(syncQrTimer); clearTimeout(syncQrTimer);
syncQrTimer = setTimeout(syncQrHide, 60000); syncQrTimer = setTimeout(syncQrHide, 60000);
+62 -21
View File
@@ -36,7 +36,7 @@ uses
FMX.Types, FMX.Controls, FMX.Forms, FMX.Graphics, FMX.Dialogs, FMX.Platform, FMX.Types, FMX.Controls, FMX.Forms, FMX.Graphics, FMX.Dialogs, FMX.Platform,
FMX.Controls.Presentation, FMX.StdCtrls, FMX.Memo, FMX.Memo.Types, FMX.Controls.Presentation, FMX.StdCtrls, FMX.Memo, FMX.Memo.Types,
FMX.ScrollBox, FMX.Layouts, FMX.Edit, FMX.ListBox, FMX.Objects, FMX.ScrollBox, FMX.Layouts, FMX.Edit, FMX.ListBox, FMX.Objects,
FMX.WebBrowser, FMX.WebBrowser, System.Messaging,
PMA.Vault; PMA.Vault;
const const
@@ -342,6 +342,10 @@ type
procedure SetUnlockCode(const ANew: string); procedure SetUnlockCode(const ANew: string);
procedure PushUnlockMode; procedure PushUnlockMode;
procedure PasteSyncSettings; procedure PasteSyncSettings;
function ApplySyncLink(const ALink: string): Boolean;
{$IFDEF ANDROID}
procedure OnActivityMessage(const Sender: TObject; const M: TMessage);
{$ENDIF}
function PwdName: string; function PwdName: string;
/// Va chercher l'icone du site de l'entree ouverte, ou l'efface. /// Va chercher l'icone du site de l'entree ouverte, ou l'efface.
procedure SetEntryIcon(AFetch: Boolean); procedure SetEntryIcon(AFetch: Boolean);
@@ -381,7 +385,11 @@ implementation
uses uses
PMA.Crypto, PMA.SyncClient, PMA.Store, PMA.Biometric, PMA.Totp, PMA.Crypto, PMA.SyncClient, PMA.Store, PMA.Biometric, PMA.Totp,
PMA.ScreenGuard, PMA.Clipboard, PMA.Theme, PMA.WebUI, PMA.VaultPatch, PMA.ScreenGuard, PMA.Clipboard, PMA.Theme, PMA.WebUI, PMA.VaultPatch,
PMA.SaveFile, PMA.Favicon, System.JSON; PMA.SaveFile, PMA.Favicon, System.JSON
{$IFDEF ANDROID}
, Androidapi.JNI.GraphicsContentViewText, Androidapi.JNI.JavaTypes,
Androidapi.Helpers, FMX.Platform.Android
{$ENDIF};
{$R *.fmx} {$R *.fmx}
@@ -668,6 +676,18 @@ begin
FWeb.OnDidFinishLoad := OnWebLoaded; FWeb.OnDidFinishLoad := OnWebLoaded;
FWeb.OnShouldStartLoadWithRequest := OnWebNavigate; FWeb.OnShouldStartLoadWithRequest := OnWebNavigate;
FWeb.LoadFromStrings(PageHtml, ''); FWeb.LoadFromStrings(PageHtml, '');
{$IFDEF ANDROID}
// LIEN DU QR DU POSTE. Au lancement par le lien, il arrive dans l'intent de
// depart ; app deja ouverte (singleTask), il arrive par message. WebCall met
// en attente tant que la page n'est pas chargee.
MainActivity.registerIntentAction(TJIntent.JavaClass.ACTION_VIEW);
TMessageManager.DefaultManager.SubscribeToMessage(TMessageReceivedNotification,
OnActivityMessage);
if (TAndroidHelper.Activity.getIntent <> nil) and
(TAndroidHelper.Activity.getIntent.getDataString <> nil) then
ApplySyncLink(JStringToString(TAndroidHelper.Activity.getIntent.getDataString));
{$ENDIF}
end; end;
procedure TForm8.OnWebLoaded(ASender: TObject); procedure TForm8.OnWebLoaded(ASender: TObject);
@@ -3206,33 +3226,54 @@ begin
end; end;
procedure TForm8.PasteSyncSettings; procedure TForm8.PasteSyncSettings;
var
LDoc: TJSONObject;
LUrl, LTok: string;
begin begin
if ApplySyncLink(CurrentClipboard.Trim) then
ClearClipboard
else
WebCall('PM.toast("No settings in the clipboard - copy the link of the QR shown on the PC first")');
end;
/// pmserver://sync?u=URL&t=JETON -> champs des reglages remplis, RIEN
/// d'enregistre. Le lien peut venir de n'importe quelle page ou application :
/// c'est Save, geste de l'utilisateur devant l'URL affichee, qui valide.
function TForm8.ApplySyncLink(const ALink: string): Boolean;
var
LPair: string;
LUrl, LTok, LName, LValue: string;
LEq: Integer;
begin
Result := False;
if not StartsText('pmserver://sync?', ALink) then Exit;
LUrl := ''; LUrl := '';
LTok := ''; LTok := '';
LDoc := TJSONObject.ParseJSONValue(CurrentClipboard.Trim) as TJSONObject; for LPair in Copy(ALink, Length('pmserver://sync?') + 1, MaxInt).Split(['&']) do
if LDoc <> nil then
try
if LDoc.GetValue<Integer>('pmsync', 0) = 1 then
begin begin
LUrl := LDoc.GetValue<string>('u', ''); LEq := Pos('=', LPair);
LTok := LDoc.GetValue<string>('t', ''); if LEq = 0 then Continue;
LName := Copy(LPair, 1, LEq - 1);
LValue := TNetEncoding.URL.Decode(Copy(LPair, LEq + 1, MaxInt));
if LName = 'u' then LUrl := LValue
else if LName = 't' then LTok := LValue;
end; end;
finally if (LUrl = '') or (LTok = '') then Exit;
LDoc.Free; Result := True;
end; Log('Sync settings received from a link: token ' + Fingerprint(LTok));
if (LUrl = '') or (LTok = '') then WebCall('openSettings()');
begin
WebCall('PM.toast("No settings in the clipboard - scan the QR on the PC and copy its text first")');
Exit;
end;
ClearClipboard;
Log('Sync settings pasted from the clipboard: token ' + Fingerprint(LTok));
WebCall(Format('PM.pasteConfig(%s, %s)', [JsArg(LUrl), JsArg(LTok)])); WebCall(Format('PM.pasteConfig(%s, %s)', [JsArg(LUrl), JsArg(LTok)]));
end; end;
{$IFDEF ANDROID}
procedure TForm8.OnActivityMessage(const Sender: TObject; const M: TMessage);
var
LIntent: JIntent;
begin
if not (M is TMessageReceivedNotification) then Exit;
LIntent := TMessageReceivedNotification(M).Value;
if (LIntent <> nil) and (LIntent.getDataString <> nil) then
ApplySyncLink(JStringToString(LIntent.getDataString));
end;
{$ENDIF}
function TForm8.CachePwd: string; function TForm8.CachePwd: string;
begin begin
if HasLocalContainer then Result := FVaultPwd else Result := FSyncPwd; if HasLocalContainer then Result := FVaultPwd else Result := FSyncPwd;