Compare commits
4 Commits
9d74839ec3
...
dcb1ac63ce
| Author | SHA1 | Date | |
|---|---|---|---|
| dcb1ac63ce | |||
| e3904236a7 | |||
| 72f9acd733 | |||
| 508d5345bf |
@@ -768,6 +768,16 @@ Sidebar Tools expose 2 items MFA :
|
|||||||
secret base32 aléatoire 20 bytes (`randomBase32Secret()`). Rien
|
secret base32 aléatoire 20 bytes (`randomBase32Secret()`). Rien
|
||||||
n'est sauvegardé.
|
n'est sauvegardé.
|
||||||
|
|
||||||
|
**Scanner le QR à l'écran** (bouton QR du champ 2FA de l'éditeur, 2026-09-18) :
|
||||||
|
`cmd://qr/screen` → la fenêtre passe au tray, `PM.ScreenQr` capture **tout le
|
||||||
|
bureau virtuel** en mémoire (GDI, octets bruts, jamais sur disque) et le décode
|
||||||
|
par ZXing.Delphi — le même que l'APK, `android-client\ThirdParty\ZXing` dans le
|
||||||
|
chemin de recherche du `.dproj`. Pas de sélection de zone : le décodeur trouve
|
||||||
|
le QR où qu'il soit. **Seul un `otpauth://` repart vers la page**
|
||||||
|
(`Bridge.onScreenQr`), le reste de l'écran ne quitte pas l'exe. Format `BGR32`
|
||||||
|
et non `BGRA32` : l'octet alpha d'une capture GDI vaut 0, lu comme transparent
|
||||||
|
→ image blanche.
|
||||||
|
|
||||||
Code TOTP retourne `{ code, period, secondsLeft }` — attention au nom,
|
Code TOTP retourne `{ code, period, secondsLeft }` — attention au nom,
|
||||||
**pas `remaining`** (utiliser `t.secondsLeft`).
|
**pas `remaining`** (utiliser `t.secondsLeft`).
|
||||||
|
|
||||||
|
|||||||
@@ -364,7 +364,7 @@ procedure TRGBLuminanceSource.CalculateLuminanceRGB24(
|
|||||||
const rgbRawBytes: TArray<Byte>);
|
const rgbRawBytes: TArray<Byte>);
|
||||||
var
|
var
|
||||||
rgbIndex,
|
rgbIndex,
|
||||||
luminanceIndex,
|
luminanceIndex : Integer; // pmserver: etaient Byte -> boucle infinie au-dela de 255 octets
|
||||||
r, g, b : Integer;
|
r, g, b : Integer;
|
||||||
begin
|
begin
|
||||||
rgbIndex := 0;
|
rgbIndex := 0;
|
||||||
@@ -389,7 +389,7 @@ procedure TRGBLuminanceSource.CalculateLuminanceBGR24(
|
|||||||
const rgbRawBytes: TArray<Byte>);
|
const rgbRawBytes: TArray<Byte>);
|
||||||
var
|
var
|
||||||
rgbIndex,
|
rgbIndex,
|
||||||
luminanceIndex,
|
luminanceIndex : Integer; // pmserver: etaient Byte -> boucle infinie au-dela de 255 octets
|
||||||
b, g, r : Byte;
|
b, g, r : Byte;
|
||||||
begin
|
begin
|
||||||
rgbIndex := 0;
|
rgbIndex := 0;
|
||||||
@@ -414,7 +414,7 @@ procedure TRGBLuminanceSource.CalculateLuminanceRGB32(
|
|||||||
const rgbRawBytes: TArray<Byte>);
|
const rgbRawBytes: TArray<Byte>);
|
||||||
var
|
var
|
||||||
rgbIndex,
|
rgbIndex,
|
||||||
luminanceIndex,
|
luminanceIndex : Integer; // pmserver: etaient Byte -> boucle infinie au-dela de 255 octets
|
||||||
r, g, b : Byte;
|
r, g, b : Byte;
|
||||||
begin
|
begin
|
||||||
rgbIndex := 0;
|
rgbIndex := 0;
|
||||||
@@ -439,7 +439,7 @@ procedure TRGBLuminanceSource.CalculateLuminanceBGR32(
|
|||||||
const rgbRawBytes: TArray<Byte>);
|
const rgbRawBytes: TArray<Byte>);
|
||||||
var
|
var
|
||||||
rgbIndex,
|
rgbIndex,
|
||||||
luminanceIndex,
|
luminanceIndex : Integer; // pmserver: etaient Byte -> boucle infinie au-dela de 255 octets
|
||||||
b, g, r : Byte;
|
b, g, r : Byte;
|
||||||
begin
|
begin
|
||||||
rgbIndex := 0;
|
rgbIndex := 0;
|
||||||
@@ -464,7 +464,7 @@ procedure TRGBLuminanceSource.CalculateLuminanceBGRA32(
|
|||||||
const rgbRawBytes: TArray<Byte>);
|
const rgbRawBytes: TArray<Byte>);
|
||||||
var
|
var
|
||||||
rgbIndex,
|
rgbIndex,
|
||||||
luminanceIndex,
|
luminanceIndex : Integer; // pmserver: etaient Byte -> boucle infinie au-dela de 255 octets
|
||||||
b, g, r,
|
b, g, r,
|
||||||
alpha : Byte;
|
alpha : Byte;
|
||||||
luminance : Byte;
|
luminance : Byte;
|
||||||
@@ -495,7 +495,7 @@ procedure TRGBLuminanceSource.CalculateLuminanceRGBA32(
|
|||||||
const rgbRawBytes: TArray<Byte>);
|
const rgbRawBytes: TArray<Byte>);
|
||||||
var
|
var
|
||||||
rgbIndex,
|
rgbIndex,
|
||||||
luminanceIndex,
|
luminanceIndex : Integer; // pmserver: etaient Byte -> boucle infinie au-dela de 255 octets
|
||||||
r, g, b,
|
r, g, b,
|
||||||
alpha : Byte;
|
alpha : Byte;
|
||||||
luminance : Byte;
|
luminance : Byte;
|
||||||
@@ -526,7 +526,7 @@ procedure TRGBLuminanceSource.CalculateLuminanceARGB32(
|
|||||||
const rgbRawBytes: TArray<Byte>);
|
const rgbRawBytes: TArray<Byte>);
|
||||||
var
|
var
|
||||||
rgbIndex,
|
rgbIndex,
|
||||||
luminanceIndex,
|
luminanceIndex : Integer; // pmserver: etaient Byte -> boucle infinie au-dela de 255 octets
|
||||||
r, g, b,
|
r, g, b,
|
||||||
alpha : Byte;
|
alpha : Byte;
|
||||||
luminance : Byte;
|
luminance : Byte;
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ uses
|
|||||||
PM.Licence in 'Source\PM.Licence.pas',
|
PM.Licence in 'Source\PM.Licence.pas',
|
||||||
PM.LicenceGate in 'Source\PM.LicenceGate.pas',
|
PM.LicenceGate in 'Source\PM.LicenceGate.pas',
|
||||||
DelphiZXingQRCode in 'Source\DelphiZXingQRCode.pas',
|
DelphiZXingQRCode in 'Source\DelphiZXingQRCode.pas',
|
||||||
|
PM.ScreenQr in 'Source\PM.ScreenQr.pas',
|
||||||
PM.AutoStart in 'Source\PM.AutoStart.pas',
|
PM.AutoStart in 'Source\PM.AutoStart.pas',
|
||||||
PM.Favicon in 'Source\PM.Favicon.pas',
|
PM.Favicon in 'Source\PM.Favicon.pas',
|
||||||
PM.ProcessLockdown in 'Source\PM.ProcessLockdown.pas',
|
PM.ProcessLockdown in 'Source\PM.ProcessLockdown.pas',
|
||||||
|
|||||||
@@ -91,6 +91,7 @@
|
|||||||
<Base>true</Base>
|
<Base>true</Base>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<PropertyGroup Condition="'$(Base)'!=''">
|
<PropertyGroup Condition="'$(Base)'!=''">
|
||||||
|
<DCC_UnitSearchPath>..\android-client\ThirdParty\ZXing;..\android-client\ThirdParty\ZXing\1D Barcodes;..\android-client\ThirdParty\ZXing\2D Barcodes;..\android-client\ThirdParty\ZXing\2D Barcodes\Decoder;..\android-client\ThirdParty\ZXing\2D Barcodes\Detector;..\android-client\ThirdParty\ZXing\Common;..\android-client\ThirdParty\ZXing\Common\Detector;..\android-client\ThirdParty\ZXing\Common\ReedSolomon;..\android-client\ThirdParty\ZXing\Filtering;$(DCC_UnitSearchPath)</DCC_UnitSearchPath>
|
||||||
<DCC_E>false</DCC_E>
|
<DCC_E>false</DCC_E>
|
||||||
<DCC_F>false</DCC_F>
|
<DCC_F>false</DCC_F>
|
||||||
<DCC_K>false</DCC_K>
|
<DCC_K>false</DCC_K>
|
||||||
@@ -250,6 +251,7 @@ $(PreBuildEvent)]]></PreBuildEvent>
|
|||||||
<DCCReference Include="Source\PM.Licence.pas"/>
|
<DCCReference Include="Source\PM.Licence.pas"/>
|
||||||
<DCCReference Include="Source\PM.LicenceGate.pas"/>
|
<DCCReference Include="Source\PM.LicenceGate.pas"/>
|
||||||
<DCCReference Include="Source\DelphiZXingQRCode.pas"/>
|
<DCCReference Include="Source\DelphiZXingQRCode.pas"/>
|
||||||
|
<DCCReference Include="Source\PM.ScreenQr.pas"/>
|
||||||
<DCCReference Include="Source\PM.AutoStart.pas"/>
|
<DCCReference Include="Source\PM.AutoStart.pas"/>
|
||||||
<DCCReference Include="Source\PM.Favicon.pas"/>
|
<DCCReference Include="Source\PM.Favicon.pas"/>
|
||||||
<DCCReference Include="Source\PM.ProcessLockdown.pas"/>
|
<DCCReference Include="Source\PM.ProcessLockdown.pas"/>
|
||||||
|
|||||||
@@ -174,6 +174,8 @@ type
|
|||||||
// Barrette en icones (police Segoe MDL2 Assets) plutot qu'en mots, et sa
|
// Barrette en icones (police Segoe MDL2 Assets) plutot qu'en mots, et sa
|
||||||
// largeur reelle - elle depend des boutons choisis dans les reglages.
|
// largeur reelle - elle depend des boutons choisis dans les reglages.
|
||||||
FLazyIcons: Boolean;
|
FLazyIcons: Boolean;
|
||||||
|
// Fond de la barrette retire (cle de couleur) : seuls les boutons restent.
|
||||||
|
FLazyClear: Boolean;
|
||||||
FLazyW: Integer;
|
FLazyW: Integer;
|
||||||
procedure LazyWindowHandler(var AMsg: TMessage);
|
procedure LazyWindowHandler(var AMsg: TMessage);
|
||||||
// Corps extraits des raccourcis Ctrl+Shift+A et Ctrl+Shift+Q : la barrette
|
// Corps extraits des raccourcis Ctrl+Shift+A et Ctrl+Shift+Q : la barrette
|
||||||
@@ -215,7 +217,7 @@ type
|
|||||||
// it before we came out of the tray. False for callers that already decided
|
// it before we came out of the tray. False for callers that already decided
|
||||||
// who should be focused (post-autofill hands it to the filled window).
|
// who should be focused (post-autofill hands it to the filled window).
|
||||||
procedure MinimizeToTray(AClearClipboard: Boolean = True;
|
procedure MinimizeToTray(AClearClipboard: Boolean = True;
|
||||||
AReturnForeground: Boolean = True);
|
AReturnForeground: Boolean = True; ANotify: Boolean = True);
|
||||||
// Record the application currently in the foreground as the one to restore
|
// Record the application currently in the foreground as the one to restore
|
||||||
// focus to when we go back to the tray. Ignores our own and shell windows.
|
// focus to when we go back to the tray. Ignores our own and shell windows.
|
||||||
procedure CaptureForegroundOwner;
|
procedure CaptureForegroundOwner;
|
||||||
@@ -446,6 +448,7 @@ const
|
|||||||
LAZY_ID_SETTINGS = 108; // reglages, onglet Fill bar
|
LAZY_ID_SETTINGS = 108; // reglages, onglet Fill bar
|
||||||
LAZY_MENU_ICONS = 1; // hors des valeurs d'opacite (25..100) du meme menu
|
LAZY_MENU_ICONS = 1; // hors des valeurs d'opacite (25..100) du meme menu
|
||||||
LAZY_MENU_SETTINGS = 2;
|
LAZY_MENU_SETTINGS = 2;
|
||||||
|
LAZY_MENU_CLEAR = 3;
|
||||||
// LE SONDAGE PLUTOT QUE TrackMouseEvent. La barrette a quatre fenetres
|
// LE SONDAGE PLUTOT QUE TrackMouseEvent. La barrette a quatre fenetres
|
||||||
// enfants : passer d'elle a un bouton envoie deja un WM_MOUSELEAVE au parent,
|
// enfants : passer d'elle a un bouton envoie deja un WM_MOUSELEAVE au parent,
|
||||||
// et il faudrait donc suivre la souris sur les cinq fenetres pour recoller
|
// et il faudrait donc suivre la souris sur les cinq fenetres pour recoller
|
||||||
@@ -469,6 +472,12 @@ const
|
|||||||
LAZY_BTNS_KEY = 'lazyButtons';
|
LAZY_BTNS_KEY = 'lazyButtons';
|
||||||
LAZY_BTNS_DEFAULT = 'full,pwd,totp,add,search,open,settings';
|
LAZY_BTNS_DEFAULT = 'full,pwd,totp,add,search,open,settings';
|
||||||
LAZY_ICONS_KEY = 'lazyIcons';
|
LAZY_ICONS_KEY = 'lazyIcons';
|
||||||
|
// FOND TRANSPARENT : peint dans une couleur qu'aucun theme n'emploie, puis
|
||||||
|
// declaree cle (LWA_COLORKEY) - ces pixels ne sont plus dessines du tout, et
|
||||||
|
// les clics y TRAVERSENT. On traine donc la barrette par ses boutons
|
||||||
|
// (DragDetect), le fond n'etant plus la pour etre saisi.
|
||||||
|
LAZY_CLEAR_KEY = 'lazyClear';
|
||||||
|
LAZY_KEY_COLOR = $00FF00FF;
|
||||||
LAZY_H = 40;
|
LAZY_H = 40;
|
||||||
|
|
||||||
// Palette de la barrette, en COLORREF ($00BBGGRR - les octets sont INVERSES
|
// Palette de la barrette, en COLORREF ($00BBGGRR - les octets sont INVERSES
|
||||||
@@ -826,7 +835,7 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TPMBridge.MinimizeToTray(AClearClipboard: Boolean = True;
|
procedure TPMBridge.MinimizeToTray(AClearClipboard: Boolean = True;
|
||||||
AReturnForeground: Boolean = True);
|
AReturnForeground: Boolean = True; ANotify: Boolean = True);
|
||||||
var
|
var
|
||||||
LFormHwnd, LAppHwnd: HWND;
|
LFormHwnd, LAppHwnd: HWND;
|
||||||
begin
|
begin
|
||||||
@@ -884,7 +893,9 @@ begin
|
|||||||
// 3. First-time only: pop a balloon notification so the user knows the
|
// 3. First-time only: pop a balloon notification so the user knows the
|
||||||
// app is still running in the tray (and didn't crash). Skipped when
|
// app is still running in the tray (and didn't crash). Skipped when
|
||||||
// the user opted out via Settings.
|
// the user opted out via Settings.
|
||||||
if (not FBalloonShown) and FShowNotifications then
|
// ANotify=False : absence de quelques centaines de ms (scan du QR), la
|
||||||
|
// fenetre revient seule - l'annonce "toujours dans le tray" serait fausse.
|
||||||
|
if ANotify and (not FBalloonShown) and FShowNotifications then
|
||||||
begin
|
begin
|
||||||
ShowFirstTimeBalloon;
|
ShowFirstTimeBalloon;
|
||||||
FBalloonShown := True;
|
FBalloonShown := True;
|
||||||
@@ -1267,15 +1278,22 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TPMBridge.ApplyLazyAlpha(AHover: Boolean);
|
procedure TPMBridge.ApplyLazyAlpha(AHover: Boolean);
|
||||||
|
var
|
||||||
|
LAlpha: Byte;
|
||||||
begin
|
begin
|
||||||
if FLazyWnd = 0 then Exit;
|
if FLazyWnd = 0 then Exit;
|
||||||
// TRANSPARENTE N'EST PAS TRAVERSANTE : la barrette continue d'avaler les
|
// TRANSPARENTE N'EST PAS TRAVERSANTE : la barrette continue d'avaler les
|
||||||
// clics sur toute sa surface. C'est un allegement du regard, pas un passage.
|
// clics sur toute sa surface. C'est un allegement du regard, pas un passage.
|
||||||
|
// Le pourcentage s'applique toujours aux boutons, fond retire ou non.
|
||||||
if AHover or (FLazyAlphaPct >= 100) then
|
if AHover or (FLazyAlphaPct >= 100) then
|
||||||
SetLayeredWindowAttributes(FLazyWnd, 0, 255, LWA_ALPHA)
|
LAlpha := 255
|
||||||
else
|
else
|
||||||
SetLayeredWindowAttributes(FLazyWnd, 0,
|
LAlpha := MulDiv(255, FLazyAlphaPct, 100);
|
||||||
MulDiv(255, FLazyAlphaPct, 100), LWA_ALPHA);
|
if FLazyClear then
|
||||||
|
SetLayeredWindowAttributes(FLazyWnd, LAZY_KEY_COLOR, LAlpha,
|
||||||
|
LWA_ALPHA or LWA_COLORKEY)
|
||||||
|
else
|
||||||
|
SetLayeredWindowAttributes(FLazyWnd, 0, LAlpha, LWA_ALPHA);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TPMBridge.ShowLazyMenu;
|
procedure TPMBridge.ShowLazyMenu;
|
||||||
@@ -1298,6 +1316,8 @@ begin
|
|||||||
if FLazyIcons then LIconsLabel := 'Show text' else LIconsLabel := 'Show icons';
|
if FLazyIcons then LIconsLabel := 'Show text' else LIconsLabel := 'Show icons';
|
||||||
AppendMenu(LMenu, MF_SEPARATOR, 0, nil);
|
AppendMenu(LMenu, MF_SEPARATOR, 0, nil);
|
||||||
AppendMenu(LMenu, MF_STRING, LAZY_MENU_ICONS, PChar(LIconsLabel));
|
AppendMenu(LMenu, MF_STRING, LAZY_MENU_ICONS, PChar(LIconsLabel));
|
||||||
|
AppendMenu(LMenu, MF_STRING or IfThen(FLazyClear, MF_CHECKED, MF_UNCHECKED),
|
||||||
|
LAZY_MENU_CLEAR, 'Transparent background');
|
||||||
AppendMenu(LMenu, MF_STRING, LAZY_MENU_SETTINGS, 'Fill bar settings...');
|
AppendMenu(LMenu, MF_STRING, LAZY_MENU_SETTINGS, 'Fill bar settings...');
|
||||||
|
|
||||||
// LE BATTEMENT DOIT S'ARRETER PENDANT LE MENU. Il repose la barrette en
|
// LE BATTEMENT DOIT S'ARRETER PENDANT LE MENU. Il repose la barrette en
|
||||||
@@ -1321,6 +1341,11 @@ begin
|
|||||||
if Assigned(FOnTrayRestore) then FOnTrayRestore();
|
if Assigned(FOnTrayRestore) then FOnTrayRestore();
|
||||||
if Assigned(FOnFillBarSettings) then FOnFillBarSettings();
|
if Assigned(FOnFillBarSettings) then FOnFillBarSettings();
|
||||||
end
|
end
|
||||||
|
else if LCmd = LAZY_MENU_CLEAR then
|
||||||
|
begin
|
||||||
|
if FLazyClear then SetPref(LAZY_CLEAR_KEY, '0') else SetPref(LAZY_CLEAR_KEY, '1');
|
||||||
|
ReloadLazyBar;
|
||||||
|
end
|
||||||
else if LCmd = LAZY_MENU_ICONS then
|
else if LCmd = LAZY_MENU_ICONS then
|
||||||
begin
|
begin
|
||||||
if FLazyIcons then SetPref(LAZY_ICONS_KEY, '0') else SetPref(LAZY_ICONS_KEY, '1');
|
if FLazyIcons then SetPref(LAZY_ICONS_KEY, '0') else SetPref(LAZY_ICONS_KEY, '1');
|
||||||
@@ -1403,6 +1428,14 @@ begin
|
|||||||
WM_ERASEBKGND:
|
WM_ERASEBKGND:
|
||||||
begin
|
begin
|
||||||
GetClientRect(FLazyWnd, LRect);
|
GetClientRect(FLazyWnd, LRect);
|
||||||
|
if FLazyClear then
|
||||||
|
begin
|
||||||
|
LBrush := CreateSolidBrush(LAZY_KEY_COLOR);
|
||||||
|
FillRect(HDC(AMsg.WParam), LRect, LBrush);
|
||||||
|
DeleteObject(LBrush);
|
||||||
|
AMsg.Result := 1;
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
LBrush := CreateSolidBrush(IfThen(FDarkTheme, LAZY_DARK_BG, LAZY_LITE_BG));
|
LBrush := CreateSolidBrush(IfThen(FDarkTheme, LAZY_DARK_BG, LAZY_LITE_BG));
|
||||||
FillRect(HDC(AMsg.WParam), LRect, LBrush);
|
FillRect(HDC(AMsg.WParam), LRect, LBrush);
|
||||||
DeleteObject(LBrush);
|
DeleteObject(LBrush);
|
||||||
@@ -1580,6 +1613,7 @@ begin
|
|||||||
FLazyAlphaPct := LAZY_ALPHA_DEFAULT;
|
FLazyAlphaPct := LAZY_ALPHA_DEFAULT;
|
||||||
|
|
||||||
FLazyIcons := GetPref(LAZY_ICONS_KEY) = '1';
|
FLazyIcons := GetPref(LAZY_ICONS_KEY) = '1';
|
||||||
|
FLazyClear := GetPref(LAZY_CLEAR_KEY) = '1';
|
||||||
LList := GetPref(LAZY_BTNS_KEY);
|
LList := GetPref(LAZY_BTNS_KEY);
|
||||||
if LList = '' then LList := LAZY_BTNS_DEFAULT;
|
if LList = '' then LList := LAZY_BTNS_DEFAULT;
|
||||||
InitCommonControls;
|
InitCommonControls;
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
unit PM.ScreenQr;
|
||||||
|
|
||||||
|
// Lit un QR affiche a l'ecran - le secret 2FA qu'un site montre a l'activation.
|
||||||
|
// Capture de TOUS les ecrans (bureau virtuel) en memoire, jamais sur le disque,
|
||||||
|
// puis decodage par ZXing.Delphi (Apache 2.0, deja vendu pour l'APK dans
|
||||||
|
// android-client\ThirdParty\ZXing). Aucune selection de zone : le decodeur
|
||||||
|
// trouve le QR n'importe ou dans l'image.
|
||||||
|
//
|
||||||
|
// GDI pur et octets bruts, pas de TBitmap FMX : tout se fait dans un thread
|
||||||
|
// d'arriere-plan, et le decodage d'un bureau entier prend un moment.
|
||||||
|
|
||||||
|
interface
|
||||||
|
|
||||||
|
/// Texte du premier QR trouve a l'ecran, '' si aucun.
|
||||||
|
function ScanScreenForQr: string;
|
||||||
|
// Exposees pour le banc d'essai (mesure du temps de decodage).
|
||||||
|
function CaptureVirtualScreen(out AWidth, AHeight: Integer): TArray<Byte>;
|
||||||
|
function DecodeQrPixels(const APixels: TArray<Byte>; AW, AH: Integer;
|
||||||
|
ATryHarder: Boolean): string;
|
||||||
|
|
||||||
|
implementation
|
||||||
|
|
||||||
|
uses
|
||||||
|
Winapi.Windows, System.SysUtils, System.Generics.Collections,
|
||||||
|
ZXing.LuminanceSource, ZXing.RGBLuminanceSource, ZXing.HybridBinarizer,
|
||||||
|
ZXing.BinaryBitmap, ZXing.MultiFormatReader, ZXing.BarcodeFormat,
|
||||||
|
ZXing.DecodeHintType, ZXing.ReadResult;
|
||||||
|
|
||||||
|
const
|
||||||
|
CAPTURE_BLT = $40000000; // absente de Winapi.Windows ; inclut les fenetres superposees
|
||||||
|
|
||||||
|
function CaptureVirtualScreen(out AWidth, AHeight: Integer): TArray<Byte>;
|
||||||
|
var
|
||||||
|
LX, LY: Integer;
|
||||||
|
LScreenDC, LMemDC: HDC;
|
||||||
|
LBmp: HBITMAP;
|
||||||
|
LOld: HGDIOBJ;
|
||||||
|
LInfo: TBitmapInfo;
|
||||||
|
begin
|
||||||
|
Result := nil;
|
||||||
|
LX := GetSystemMetrics(SM_XVIRTUALSCREEN);
|
||||||
|
LY := GetSystemMetrics(SM_YVIRTUALSCREEN);
|
||||||
|
AWidth := GetSystemMetrics(SM_CXVIRTUALSCREEN);
|
||||||
|
AHeight := GetSystemMetrics(SM_CYVIRTUALSCREEN);
|
||||||
|
if (AWidth <= 0) or (AHeight <= 0) then Exit;
|
||||||
|
|
||||||
|
LScreenDC := GetDC(0);
|
||||||
|
LMemDC := CreateCompatibleDC(LScreenDC);
|
||||||
|
LBmp := CreateCompatibleBitmap(LScreenDC, AWidth, AHeight);
|
||||||
|
try
|
||||||
|
LOld := SelectObject(LMemDC, LBmp);
|
||||||
|
BitBlt(LMemDC, 0, 0, AWidth, AHeight, LScreenDC, LX, LY, SRCCOPY or CAPTURE_BLT);
|
||||||
|
SelectObject(LMemDC, LOld);
|
||||||
|
|
||||||
|
FillChar(LInfo, SizeOf(LInfo), 0);
|
||||||
|
LInfo.bmiHeader.biSize := SizeOf(TBitmapInfoHeader);
|
||||||
|
LInfo.bmiHeader.biWidth := AWidth;
|
||||||
|
LInfo.bmiHeader.biHeight := -AHeight; // negatif = de haut en bas
|
||||||
|
LInfo.bmiHeader.biPlanes := 1;
|
||||||
|
LInfo.bmiHeader.biBitCount := 32;
|
||||||
|
LInfo.bmiHeader.biCompression := BI_RGB;
|
||||||
|
SetLength(Result, AWidth * AHeight * 4);
|
||||||
|
if GetDIBits(LMemDC, LBmp, 0, AHeight, @Result[0], LInfo, DIB_RGB_COLORS) = 0 then
|
||||||
|
Result := nil;
|
||||||
|
finally
|
||||||
|
DeleteObject(LBmp);
|
||||||
|
DeleteDC(LMemDC);
|
||||||
|
ReleaseDC(0, LScreenDC);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function DecodeQrPixels(const APixels: TArray<Byte>; AW, AH: Integer;
|
||||||
|
ATryHarder: Boolean): string;
|
||||||
|
var
|
||||||
|
LSource: TLuminanceSource;
|
||||||
|
LBinarizer: THybridBinarizer;
|
||||||
|
LImage: TBinaryBitmap;
|
||||||
|
LReader: TMultiFormatReader;
|
||||||
|
LHints: TDictionary<TDecodeHintType, TObject>;
|
||||||
|
LFormats: TList<TBarcodeFormat>;
|
||||||
|
LRes: TReadResult;
|
||||||
|
begin
|
||||||
|
Result := '';
|
||||||
|
LFormats := TList<TBarcodeFormat>.Create;
|
||||||
|
LFormats.Add(TBarcodeFormat.QR_CODE);
|
||||||
|
LHints := TDictionary<TDecodeHintType, TObject>.Create;
|
||||||
|
LHints.Add(TDecodeHintType.POSSIBLE_FORMATS, LFormats);
|
||||||
|
if ATryHarder then LHints.Add(TDecodeHintType.TRY_HARDER, nil);
|
||||||
|
LReader := TMultiFormatReader.Create;
|
||||||
|
LReader.Hints := LHints;
|
||||||
|
// BGR32 et non BGRA32 : le 4e octet d'une capture GDI vaut 0, et la
|
||||||
|
// conversion BGRA le lirait comme une transparence totale - image blanche.
|
||||||
|
LSource := TRGBLuminanceSource.Create(APixels, AW, AH, TBitmapFormat.BGR32);
|
||||||
|
LBinarizer := THybridBinarizer.Create(LSource);
|
||||||
|
LImage := TBinaryBitmap.Create(LBinarizer);
|
||||||
|
LRes := nil;
|
||||||
|
try
|
||||||
|
try
|
||||||
|
LRes := LReader.decode(LImage, True);
|
||||||
|
if LRes <> nil then Result := Trim(LRes.Text);
|
||||||
|
except
|
||||||
|
Result := '';
|
||||||
|
end;
|
||||||
|
finally
|
||||||
|
LRes.Free;
|
||||||
|
LImage.Free;
|
||||||
|
LBinarizer.Free;
|
||||||
|
LSource.Free;
|
||||||
|
LReader.Free;
|
||||||
|
LFormats.Free;
|
||||||
|
LHints.Free;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function ScanScreenForQr: string;
|
||||||
|
var
|
||||||
|
LPixels: TArray<Byte>;
|
||||||
|
LW, LH: Integer;
|
||||||
|
begin
|
||||||
|
Result := '';
|
||||||
|
LPixels := CaptureVirtualScreen(LW, LH);
|
||||||
|
if LPixels <> nil then Result := DecodeQrPixels(LPixels, LW, LH, True);
|
||||||
|
end;
|
||||||
|
|
||||||
|
end.
|
||||||
@@ -35,7 +35,7 @@ uses
|
|||||||
FMX.TMSFNCEdgeWebBrowser,
|
FMX.TMSFNCEdgeWebBrowser,
|
||||||
{$ENDIF}
|
{$ENDIF}
|
||||||
PM.HTTPServer, PM.Bridge, PM.QuickUnlock, PM.PinUnlock, PM.UserPrefs, PM.Licence, PM.AutoStart,
|
PM.HTTPServer, PM.Bridge, PM.QuickUnlock, PM.PinUnlock, PM.UserPrefs, PM.Licence, PM.AutoStart,
|
||||||
PM.Favicon, DelphiZXingQRCode,
|
PM.Favicon, DelphiZXingQRCode, PM.ScreenQr,
|
||||||
FMX.Platform.Win, FMX.Menus; // WindowHandleToPlatform → HWND for visibility check
|
FMX.Platform.Win, FMX.Menus; // WindowHandleToPlatform → HWND for visibility check
|
||||||
const
|
const
|
||||||
// Bump on each release. Surfaced to JS via cmd://app/version, displayed
|
// Bump on each release. Surfaced to JS via cmd://app/version, displayed
|
||||||
@@ -1347,6 +1347,7 @@ begin
|
|||||||
LFb.AddPair('buttons', PM.UserPrefs.GetPref('lazyButtons'));
|
LFb.AddPair('buttons', PM.UserPrefs.GetPref('lazyButtons'));
|
||||||
LFb.AddPair('icons', TJSONBool.Create(PM.UserPrefs.GetPref('lazyIcons') = '1'));
|
LFb.AddPair('icons', TJSONBool.Create(PM.UserPrefs.GetPref('lazyIcons') = '1'));
|
||||||
LFb.AddPair('alpha', PM.UserPrefs.GetPref('lazyAlphaPct'));
|
LFb.AddPair('alpha', PM.UserPrefs.GetPref('lazyAlphaPct'));
|
||||||
|
LFb.AddPair('clear', TJSONBool.Create(PM.UserPrefs.GetPref('lazyClear') = '1'));
|
||||||
WebBrowser.ExecuteJavaScript(
|
WebBrowser.ExecuteJavaScript(
|
||||||
'if(window.Bridge&&Bridge.onFillBar)Bridge.onFillBar(' + LFb.ToJSON + ')');
|
'if(window.Bridge&&Bridge.onFillBar)Bridge.onFillBar(' + LFb.ToJSON + ')');
|
||||||
finally
|
finally
|
||||||
@@ -1359,6 +1360,8 @@ begin
|
|||||||
PM.UserPrefs.SetPref('lazyButtons', GetParam('buttons'));
|
PM.UserPrefs.SetPref('lazyButtons', GetParam('buttons'));
|
||||||
if GetParam('icons') = '1' then PM.UserPrefs.SetPref('lazyIcons', '1')
|
if GetParam('icons') = '1' then PM.UserPrefs.SetPref('lazyIcons', '1')
|
||||||
else PM.UserPrefs.SetPref('lazyIcons', '0');
|
else PM.UserPrefs.SetPref('lazyIcons', '0');
|
||||||
|
if GetParam('clear') = '1' then PM.UserPrefs.SetPref('lazyClear', '1')
|
||||||
|
else PM.UserPrefs.SetPref('lazyClear', '0');
|
||||||
var LAlpha := StrToIntDef(GetParam('alpha'), 60);
|
var LAlpha := StrToIntDef(GetParam('alpha'), 60);
|
||||||
if (LAlpha >= 25) and (LAlpha <= 100) then
|
if (LAlpha >= 25) and (LAlpha <= 100) then
|
||||||
PM.UserPrefs.SetPref('lazyAlphaPct', IntToStr(LAlpha));
|
PM.UserPrefs.SetPref('lazyAlphaPct', IntToStr(LAlpha));
|
||||||
@@ -1390,6 +1393,40 @@ begin
|
|||||||
// envoie tout texte a une recherche web (Google, Chrome) - le jeton serait
|
// 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
|
// parti chez Google a un appui pres. Un lien a schema propre n'a qu'une
|
||||||
// application pour l'ouvrir : la notre.
|
// application pour l'ouvrir : la notre.
|
||||||
|
// QR 2FA affiche par un site : la fenetre s'efface le temps de la capture
|
||||||
|
// (elle recouvre souvent le QR), puis revient. Seul un lien otpauth:// part
|
||||||
|
// vers la page - le reste de l'ecran ne quitte pas la memoire de l'exe.
|
||||||
|
else if ACmd = 'qr/screen' then
|
||||||
|
begin
|
||||||
|
LogLine('qr/screen: scanning');
|
||||||
|
FBridge.MinimizeToTray(False, False, False);
|
||||||
|
TThread.CreateAnonymousThread(
|
||||||
|
procedure
|
||||||
|
var
|
||||||
|
LText: string;
|
||||||
|
begin
|
||||||
|
Sleep(400); // laisser Windows retirer la fenetre de l'ecran
|
||||||
|
LText := ScanScreenForQr;
|
||||||
|
if Pos('otpauth://', LowerCase(LText)) <> 1 then LText := '';
|
||||||
|
TThread.Queue(nil,
|
||||||
|
procedure
|
||||||
|
var
|
||||||
|
LJs: TJSONString;
|
||||||
|
begin
|
||||||
|
RestoreWindowFromTray;
|
||||||
|
if LText = '' then LogLine('qr/screen: no otpauth QR found')
|
||||||
|
else LogLine('qr/screen: otpauth QR found');
|
||||||
|
LJs := TJSONString.Create(LText);
|
||||||
|
try
|
||||||
|
WebBrowser.ExecuteJavaScript(
|
||||||
|
'if(window.Bridge&&Bridge.onScreenQr)Bridge.onScreenQr(' + LJs.ToJSON + ')');
|
||||||
|
finally
|
||||||
|
LJs.Free;
|
||||||
|
end;
|
||||||
|
end);
|
||||||
|
end).Start;
|
||||||
|
end
|
||||||
|
|
||||||
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');
|
||||||
|
|||||||
+11
@@ -47,6 +47,7 @@
|
|||||||
<symbol id="i-printer" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 6 2 18 2 18 9"/><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"/><rect x="6" y="14" width="12" height="8"/></symbol>
|
<symbol id="i-printer" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 6 2 18 2 18 9"/><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"/><rect x="6" y="14" width="12" height="8"/></symbol>
|
||||||
<symbol id="i-pin" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 17v5"/><path d="M9 10.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24V16a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V7a1 1 0 0 1 1-1 2 2 0 0 0 0-4H8a2 2 0 0 0 0 4 1 1 0 0 1 1 1z"/></symbol>
|
<symbol id="i-pin" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 17v5"/><path d="M9 10.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24V16a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V7a1 1 0 0 1 1-1 2 2 0 0 0 0-4H8a2 2 0 0 0 0 4 1 1 0 0 1 1 1z"/></symbol>
|
||||||
<symbol id="i-paperclip" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.44 11.05 12.25 20.24a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/></symbol>
|
<symbol id="i-paperclip" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.44 11.05 12.25 20.24a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/></symbol>
|
||||||
|
<symbol id="i-qr" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><path d="M14 14h3v3h-3zM21 14v.01M14 21h.01M17 21h4v-4"/></symbol>
|
||||||
<symbol id="i-download" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></symbol>
|
<symbol id="i-download" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></symbol>
|
||||||
<symbol id="i-empty-vault" viewBox="0 0 120 120" fill="none">
|
<symbol id="i-empty-vault" viewBox="0 0 120 120" fill="none">
|
||||||
<circle cx="60" cy="60" r="50" fill="var(--accent-soft)"/>
|
<circle cx="60" cy="60" r="50" fill="var(--accent-soft)"/>
|
||||||
@@ -785,6 +786,16 @@
|
|||||||
<option value="25">25%</option>
|
<option value="25">25%</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="setting-row">
|
||||||
|
<span>
|
||||||
|
Transparent background
|
||||||
|
<small class="setting-hint">Only the buttons stay visible. Drag the bar by a button.</small>
|
||||||
|
</span>
|
||||||
|
<label class="toggle">
|
||||||
|
<input type="checkbox" id="fbClear">
|
||||||
|
<span class="toggle-slider"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="slideover-field">
|
<div class="slideover-field">
|
||||||
|
|||||||
@@ -4833,6 +4833,24 @@ function soTotpField(plainSecret) {
|
|||||||
});
|
});
|
||||||
row.appendChild(input);
|
row.appendChild(input);
|
||||||
row.appendChild(toggle);
|
row.appendChild(toggle);
|
||||||
|
// Lit le QR qu'affiche le site a l'activation de la 2FA : l'exe s'efface,
|
||||||
|
// capture l'ecran et ne renvoie qu'un lien otpauth:// (qr/screen).
|
||||||
|
if (Bridge.active) {
|
||||||
|
const scan = el('button', { class: 'icon-btn icon-btn-sm', type: 'button', title: 'Scan the 2FA QR code shown on screen' });
|
||||||
|
scan.appendChild(icon('i-qr'));
|
||||||
|
scan.addEventListener('click', () => {
|
||||||
|
Bridge.onScreenQr = (text) => {
|
||||||
|
const secret = parseOtpAuthUri(text || '');
|
||||||
|
if (!secret) { toast('No 2FA QR code found on screen', 'warning'); return; }
|
||||||
|
input.value = secret;
|
||||||
|
updateTotpDisplay();
|
||||||
|
soDirtyCheck();
|
||||||
|
toast('2FA secret read from the QR code');
|
||||||
|
};
|
||||||
|
window.location.href = 'cmd://qr/screen';
|
||||||
|
});
|
||||||
|
row.appendChild(scan);
|
||||||
|
}
|
||||||
row.appendChild(clear);
|
row.appendChild(clear);
|
||||||
wrap.appendChild(row);
|
wrap.appendChild(row);
|
||||||
|
|
||||||
@@ -6710,6 +6728,7 @@ Bridge.onFillBar = function (st) {
|
|||||||
$$('.fb-btn').forEach(c => { c.checked = keys.includes(c.dataset.key); });
|
$$('.fb-btn').forEach(c => { c.checked = keys.includes(c.dataset.key); });
|
||||||
$('#fbStyle').value = st.icons ? 'icons' : 'text';
|
$('#fbStyle').value = st.icons ? 'icons' : 'text';
|
||||||
$('#fbAlpha').value = String(parseInt(st.alpha, 10) || 60);
|
$('#fbAlpha').value = String(parseInt(st.alpha, 10) || 60);
|
||||||
|
$('#fbClear').checked = !!st.clear;
|
||||||
};
|
};
|
||||||
// Clic droit sur la barrette -> "Fill bar settings..." : l'exe a deja ramene la
|
// Clic droit sur la barrette -> "Fill bar settings..." : l'exe a deja ramene la
|
||||||
// fenetre. Les reglages s'ouvrent PAR-DESSUS une entree en cours d'edition,
|
// fenetre. Les reglages s'ouvrent PAR-DESSUS une entree en cours d'edition,
|
||||||
@@ -6728,7 +6747,8 @@ function fillBarPush() {
|
|||||||
+ '?on=' + ($('#fbOn').checked ? '1' : '0')
|
+ '?on=' + ($('#fbOn').checked ? '1' : '0')
|
||||||
+ '&buttons=' + encodeURIComponent(keys.join(',') || 'none')
|
+ '&buttons=' + encodeURIComponent(keys.join(',') || 'none')
|
||||||
+ '&icons=' + ($('#fbStyle').value === 'icons' ? '1' : '0')
|
+ '&icons=' + ($('#fbStyle').value === 'icons' ? '1' : '0')
|
||||||
+ '&alpha=' + encodeURIComponent($('#fbAlpha').value);
|
+ '&alpha=' + encodeURIComponent($('#fbAlpha').value)
|
||||||
|
+ '&clear=' + ($('#fbClear').checked ? '1' : '0');
|
||||||
}
|
}
|
||||||
|
|
||||||
function openSettings() {
|
function openSettings() {
|
||||||
@@ -8242,7 +8262,7 @@ async function init() {
|
|||||||
if (syncTest) syncTest.addEventListener('click', syncTestConnection);
|
if (syncTest) syncTest.addEventListener('click', syncTestConnection);
|
||||||
const syncQr = document.getElementById('syncQrBtn');
|
const syncQr = document.getElementById('syncQrBtn');
|
||||||
if (syncQr) syncQr.addEventListener('click', syncQrToggle);
|
if (syncQr) syncQr.addEventListener('click', syncQrToggle);
|
||||||
['#fbOn', '#fbStyle', '#fbAlpha'].forEach(sel => {
|
['#fbOn', '#fbStyle', '#fbAlpha', '#fbClear'].forEach(sel => {
|
||||||
const el = $(sel);
|
const el = $(sel);
|
||||||
if (el) el.addEventListener('change', fillBarPush);
|
if (el) el.addEventListener('change', fillBarPush);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user