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" />
</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>
<!-- FMX n'affiche pas BiometricPrompt depuis notre NativeActivity : il
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
// prefs/has). Le mot de passe de synchro n'y est JAMAIS : un QR se
// photographie par-dessus l'epaule.
// JSON nu, sans prefixe "pmsync:" : un prefixe a deux-points se lit comme un
// lien, et l'appareil photo proposait "Ouvrir avec" au lieu de copier.
// UN LIEN pmserver://sync?u=&t= ET NON DU TEXTE. Le scanner du telephone
// 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
begin
var LQrUrl := PM.UserPrefs.GetPref('syncUrl');
@@ -1346,16 +1348,13 @@ begin
var LSize := 0;
if (LQrUrl <> '') and (LQrTok <> '') then
begin
var LJson := TJSONObject.Create;
var LQr := TDelphiZXingQRCode.Create;
var LSb := TStringBuilder.Create;
try
LJson.AddPair('pmsync', TJSONNumber.Create(1));
LJson.AddPair('u', LQrUrl);
LJson.AddPair('t', LQrTok);
LQr.Encoding := qrUTF8NoBOM;
LQr.QuietZone := 4;
LQr.Data := LJson.ToJSON;
LQr.Data := 'pmserver://sync?u=' + TNetEncoding.URL.Encode(LQrUrl) +
'&t=' + TNetEncoding.URL.Encode(LQrTok);
LSize := LQr.Rows;
for var R := 0 to LQr.Rows - 1 do
for var C := 0 to LQr.Columns - 1 do
@@ -1364,7 +1363,6 @@ begin
finally
LSb.Free;
LQr.Free;
LJson.Free;
end;
end;
if LSize > 0 then
+3 -3
View File
@@ -65,9 +65,9 @@ Bridge.onSyncQr = function (size, bits) {
'background:#fff;display:block;border-radius:6px">' +
'<path d="' + d + '" fill="#000"/></svg>' +
'<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: ' +
'Settings &rarr; Paste settings. It holds the URL and the token, not the ' +
'sync password. Hidden again in 60 s.</p>';
'Scan it with the phone camera and open it with Coffre: the URL and the ' +
'token fill in, then type the sync password (never in the QR) and Save. ' +
'Hidden again in 60 s.</p>';
box.hidden = false;
clearTimeout(syncQrTimer);
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.Controls.Presentation, FMX.StdCtrls, FMX.Memo, FMX.Memo.Types,
FMX.ScrollBox, FMX.Layouts, FMX.Edit, FMX.ListBox, FMX.Objects,
FMX.WebBrowser,
FMX.WebBrowser, System.Messaging,
PMA.Vault;
const
@@ -342,6 +342,10 @@ type
procedure SetUnlockCode(const ANew: string);
procedure PushUnlockMode;
procedure PasteSyncSettings;
function ApplySyncLink(const ALink: string): Boolean;
{$IFDEF ANDROID}
procedure OnActivityMessage(const Sender: TObject; const M: TMessage);
{$ENDIF}
function PwdName: string;
/// Va chercher l'icone du site de l'entree ouverte, ou l'efface.
procedure SetEntryIcon(AFetch: Boolean);
@@ -381,7 +385,11 @@ implementation
uses
PMA.Crypto, PMA.SyncClient, PMA.Store, PMA.Biometric, PMA.Totp,
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}
@@ -668,6 +676,18 @@ begin
FWeb.OnDidFinishLoad := OnWebLoaded;
FWeb.OnShouldStartLoadWithRequest := OnWebNavigate;
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;
procedure TForm8.OnWebLoaded(ASender: TObject);
@@ -3206,33 +3226,54 @@ begin
end;
procedure TForm8.PasteSyncSettings;
var
LDoc: TJSONObject;
LUrl, LTok: string;
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 := '';
LTok := '';
LDoc := TJSONObject.ParseJSONValue(CurrentClipboard.Trim) as TJSONObject;
if LDoc <> nil then
try
if LDoc.GetValue<Integer>('pmsync', 0) = 1 then
for LPair in Copy(ALink, Length('pmserver://sync?') + 1, MaxInt).Split(['&']) do
begin
LUrl := LDoc.GetValue<string>('u', '');
LTok := LDoc.GetValue<string>('t', '');
LEq := Pos('=', LPair);
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;
finally
LDoc.Free;
end;
if (LUrl = '') or (LTok = '') then
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));
if (LUrl = '') or (LTok = '') then Exit;
Result := True;
Log('Sync settings received from a link: token ' + Fingerprint(LTok));
WebCall('openSettings()');
WebCall(Format('PM.pasteConfig(%s, %s)', [JsArg(LUrl), JsArg(LTok)]));
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;
begin
if HasLocalContainer then Result := FVaultPwd else Result := FSyncPwd;