{%MainUnit ../lclintf.pas} {$I ../../components/lazutils/lazutils_defines.inc} //LCL depends on LazUtils, so this is OK {$IFnDEF WinCE} const ASSOCSTR_COMMAND = 1; //ASSOCSTR_EXECUTABLE = 2; //ASSOCSTR_FRIENDLYDOCNAME = 3; //ASSOCSTR_FRIENDLYAPPNAME = 4; //ASSOCSTR_NOOPEN = 5; //ASSOCSTR_SHELLNEWVALUE = 6; //ASSOCSTR_DDECOMMAND = 7; //ASSOCSTR_DDEIFEXEC = 8; //ASSOCSTR_DDEAPPLICATION = 9; //ASSOCSTR_DDETOPIC = 10; //ASSOCSTR_INFOTIP = 11; //ASSOCSTR_QUICKTIP = 12; //ASSOCSTR_TILEINFO = 13; //ASSOCSTR_CONTENTTYPE = 14; //ASSOCSTR_DEFAULTICON = 15; //ASSOCSTR_SHELLEXTENSION = 16; //ASSOCSTR_DROPTARGET = 17; //ASSOCSTR_DELEGATEEXECUTE = 18; //ASSOCSTR_SUPPORTED_URI_PROTOCOLS = 19; //ASSOCSTR_PROGID = 20; ASSOCSTR_APPID = 21; //ASSOCSTR_APPPUBLISHER = 22; //ASSOCSTR_APPICONREFERENCE = 23; //ASSOCSTR_MAX = ; //ASSOCF_NONE = $00000000; //ASSOCF_INIT_NOREMAPCLSID = $00000001; //ASSOCF_INIT_BYEXENAME = $00000002; //ASSOCF_OPEN_BYEXENAME = $00000002; //ASSOCF_INIT_DEFAULTTOSTAR = $00000004; //ASSOCF_INIT_DEFAULTTOFOLDER = $00000008; //ASSOCF_NOUSERSETTINGS = $00000010; ASSOCF_NOTRUNCATE = $00000020; //ASSOCF_VERIFY = $00000040; //ASSOCF_REMAPRUNDLL = $00000080; //ASSOCF_NOFIXUPS = $00000100; //ASSOCF_IGNOREBASECLASS = $00000200; //ASSOCF_INIT_IGNOREUNKNOWN = $00000400; //ASSOCF_INIT_FIXED_PROGID = $00000800; //ASSOCF_IS_PROTOCOL = $00001000; //ASSOCF_INIT_FOR_FILE = $00002000; const //List of WinAppBrwosers (Win 10) that are capable of handling local filenames with anchors //Strings must be in uppercase //The string must be the "easy part" that can be detected in a AppUserModelID like //shell:AppsFolder\Microsoft.MicrosoftEdge_8wekyb3d8bbwe!MicrosoftEdge //Currently Edge is the only one that can handle this, but others may follow CapableWinAppBrowsers: Array[1..1] of WideString = ( 'MICROSOFTEDGE' ); function AssocQueryStringW(Flags: Integer; Str: Integer; pszAssoc, pszExtra, pszOut: PWChar; var pcchOut: DWORD): HRESULT; stdcall; external 'shlwapi.dll' name 'AssocQueryStringW'; function IsLaunchWinApp(ABrowser: WideString): Boolean; begin Result := (Pos('LAUNCHWINAPP.EXE', WideUpperCase(ABrowser)) > 0) end; //not every AppUserModelID we retrieve using GetDefaultBrowserWideByAppID //accepts paramters (e.g. the URL) function LaunchWinAppBrowserCanHandleParams(ABrowser: WideString): Boolean; var i: Integer; begin Result := False; for i := Low(CapableWinAppBrowsers) to High(CapableWinAppBrowsers) do if (Pos(CapableWinAppBrowsers[i], WideUpperCase(ABrowser)) > 0) then Exit(True); end; function GetDefaultBrowserWideByAppID: WideString; const Extension = '.htm'; var BufSize: DWORD; begin BufSize := MAX_PATH; SetLength(Result, BufSize); if AssocQueryStringW(ASSOCF_NOTRUNCATE, ASSOCSTR_APPID, PWChar(Extension), 'open', PWChar(Result), BufSize) = S_OK then SetLength(Result, BufSize - 1) else Result := ''; if (Result <> '') then Result := 'shell:AppsFolder\' + Result; end; function GetDefaultBrowserWideByCmd: WideString; const Extension = '.htm'; var BufSize: DWORD; begin BufSize := MAX_PATH; SetLength(Result, BufSize); if AssocQueryStringW(ASSOCF_NOTRUNCATE, ASSOCSTR_COMMAND, PWChar(Extension), 'open', PWChar(Result), BufSize) = S_OK then SetLength(Result, BufSize - 1) else Result := ''; end; procedure ExtractBrowserAndParamsWide(const S: WideString; out ABrowser, AParams: WideString); var P: Integer; begin ABrowser := S; AParams := '%s'; if length(S) < 4 then Exit; //minimal executable name: a.exe if S[1] = '"' then begin P := 2; while (P <= length(S)) and (S[P] <> '"') do Inc(P); if P > length(S) then Exit; //malformed string: "abc foo bar ABrowser := Copy(S, 1, P); AParams := Trim(Copy(S, P+1, MaxInt)); end else begin P := Pos(#32,S); if (P = 0) then begin ABrowser := S; AParams := '"%s"'; end else begin ABrowser := Copy(S, 1, P-1); AParams := Trim(Copy(S, P+1, MaxInt)); end; end; AParams := UnicodeStringReplace(AParams, '%1', '%s', []); end; {$ENDIF WinCE} function FindDefaultBrowserWide(out ABrowser, AParams: WideString): Boolean; var AnsiBrowser, AnsiParams: String; QueryRes, SavedBrowser, SavedParams: WideString; begin ABrowser := ''; AParams := '"%s"'; {$IFnDEF WinCE} QueryRes := GetDefaultBrowserWideByCmd; if (QueryRes = '') then begin if FindBrowserExecutable('rundll32', AnsiBrowser) then begin AParams := 'url.dll,FileProtocolHandler "%s"'; {$IFnDEF ACP_RTL} ABrowser := Utf8ToUTF16(AnsiBrowser); {$else} ABrowser := WideString(AnsiBrowser); {$ENDIF ACP_RTL} end end else begin ExtractBrowserAndParamsWide(QueryRes, ABrowser, AParams); // On Windows 10, the default loading of files is done by LaunchWinApp. It calls // the linked default program. We have to find it and use it, without quotation marks! // See http://bugs.freepascal.org/view.php?id=30326 // Till now, only Edge is working correct if IsLaunchWinApp(ABrowser) then begin SavedBrowser := ABrowser; SavedParams := AParams; ABrowser := GetDefaultBrowserWideByAppID; if LaunchWinAppBrowserCanHandleParams(ABrowser) then AParams := '%s' //Edge seems to require that AParams is NOT double quoted else begin // not MS Edge (or compatible w.r.t. arguments) ABrowser := SavedBrowser; AParams := SavedParams; end; end; end; {$ENDIF} Result := (ABrowser <> ''); if not Result then begin Result := FindPredefinedBrowser(AnsiBrowser, AnsiParams); if Result then begin {$IFnDEF ACP_RTL} ABrowser := Utf8ToUtf16(AnsiBrowser); AParams := Utf8ToUtf16(AnsiParams); {$else} ABrowser := WideString(AnsiBrowser); AParams := WideString(AnsiParams); {$ENDIF ACP_RTL} end; end; end; function FindDefaultBrowserUtf8(out ABrowser, AParams: String): Boolean; var QueryRes: String; WideBrowser, WideParams: WideString; begin Result := FindDefaultBrowserWide(WideBrowser, WideParams); ABrowser := Utf16ToUtf8(WideBrowser); AParams := Utf16ToUtf8(WideParams); end; function FindDefaultBrowser(out ABrowser, AParams: String): Boolean; begin Result := FindDefaultBrowserUtf8(ABrowser, AParams); {$IFDEF ACP_RTL} ABrowser := Utf8ToWinCp(ABrowser); AParams := Utf8ToWinCp(AParams); {$ENDIF ACP_RTL} end; {$IFnDEF WinCE} function IsFileUriScheme(const AURL: String): Boolean; const FileURIScheme = 'file://'; begin Result := (CompareText(Copy(AURL,1,Length(FileURIScheme)), FileURIScheme) = 0); end; function IsHtmlWithAnchor(AURL: String): Boolean; var AnchorPos, HtmlPos: SizeInt; begin Result := False; //Anchor will be defined by last '#' in AURL; AnchorPos := Length(AURL); while (AnchorPos < 0) and (AURL[AnchorPos] <> '#') do Dec(AnchorPos); if (AnchorPos > 0) then begin AURL := UpperCase(AURL); //don't care about UTF8 HtmlPos := Pos('.HTM', AURL); if (HtmlPos = 0) then HtmlPos := Pos('.HTML', AURL); Result := (HtmlPos > 0) and (AnchorPos > HtmlPos); end; end; //Currently only used to open a local html file with a specified anchor //but in theory should be able to handle all URL's function FindDefaultBrowserAndOpenUrl(AURL: String; IsFileURI: Boolean=False{; IsLocalWithAnchor: Boolean=False}): Boolean; var ABrowser, AParams: WideString; H: HINST; AParamsUtf8: String; begin Result := False; if AURL = '' then Exit; if FindDefaultBrowserWide(ABrowser, AParams)then begin if (Pos('%s', AParams) > 0) then begin //MS IE returns quoted or unquoted %s, depending on version and OS //file:// needs to be quoted if filename contains spaces if (Pos('"%s"', AParams) = 0) and IsFileUri and (not LaunchWinAppBrowserCanHandleParams(ABrowser)) then AURL := '"'+ AURL + '"'; //at least FireFox does not like -url -osint "%s" for local files, it wants "%s" //if IsFileUri and IsLocalWithAnchor then // AParams := '"%s"'; {$IFnDEF ACP_RTL} AParamsUtf8 := Utf16ToUtf8(AParams); {$ELSE} AParamsUtf8 := AParams; {$ENDIF ACP_RTL} AParamsUtf8 := Format(AParamsUtf8,[AURL]); {$IFnDEF ACP_RTL} AParams := Utf8ToUtf16(AParamsUtf8); {$ELSE} AParams := WideString(AParamsUtf8); {$ENDIF ACP_RTL} end else begin //file:// needs to be quoted if filename contains spaces if IsFileURI and (Pos(#32, AURL) > 0) {and (not LaunchWinAppBrowserCanHandleParams(ABrowser))} then AURL := '"' + AURL + '"'; {$IFnDEF ACP_RTL} AParams := Utf8ToUtf16(AURL); {$ELSE} AParams := WideString(AURL); {$ENDIF ACP_RTL} end; //debugln('FindDefaultBrowserAndOpenUrl:'); //debugln([' ABrowser = ',ABrowser]); //debugln([' AParams = ',AParams]); H := ShellExecuteW(0, 'open', PWChar(ABrowser), PWChar(AParams), nil, SW_SHOWNORMAL); end //FindDefaultBrowserWide else begin {$IFnDEF ACP_RTL} AParams := Utf8ToUtf16(AURL); {$ELSE} AParams := WideString(AURL); {$ENDIF ACP_RTL} H := ShellExecuteW(0, nil, PWideChar(AParams), nil, nil, SW_SHOWNORMAL) ; end; Result := (H > 32); end; {$ENDIF WinCE} // Open a given URL with whatever Windows thinks is appropriate function OpenURL(AURL: String): Boolean; var {$IFDEF WinCE} Info: SHELLEXECUTEINFO; {$ELSE} ws: WideString; ans: AnsiString; IsFileUriWithSpaces, IsFileURI: Boolean; {$ENDIF} begin Result := False; if AURL = '' then Exit; {$IFDEF WinCE} FillChar(Info, SizeOf(Info), 0); Info.cbSize := SizeOf(Info); Info.fMask := SEE_MASK_FLAG_NO_UI; Info.lpVerb := 'open'; Info.lpFile := PWideChar(UTF8Decode(AURL)); Result := ShellExecuteEx(@Info); {$ELSE} IsFileURI := IsFileUriScheme(AURL); //Html FileURI's that have a local anchor cannot be opened via a direct call to ShellExecute, //in that case we need to find the actual default browser and execute that. //Notice that this will still fail to open the html at the correct anchor //if FindDefaultBrowserWide returns 'rundll.exe' //See: issue #0030326 and related if IsFileURI and IsHtmlWithAnchor(AURL) then Result := FindDefaultBrowserAndOpenURL(AURL, True{, True}) else begin //Urls that start with file:// are allowed to contain spaces and should be quoted //Since on Windows filenames cannot contain the " character, we need not care about it and simply enclose the AURL IsFileUriWithSpaces := IsFileURI and (Pos(#32,AURL) > 0); if IsFileUriWithSpaces then AURL := '"' + AURL + '"'; ws := UTF8Decode(AURL); Result := ShellExecuteW(0, nil, PWideChar(ws), nil, nil, SW_SHOWNORMAL) > 32; end; {$ENDIF} end; // Open a document with the default application associated with it in the system function OpenDocument(APath: String): Boolean; begin Result := OpenURL(APath); end; function SelectInFolder(AFullPath: String): Boolean; begin Result := FileExists(AFullPath) or DirectoryExists(AFullPath); if Result then try // quotes are required even in the absence of spaces // the comma at the end "/select," is required // do not check the return code ExecuteProcess('explorer.exe', '/select,"' + AFullPath + '"'); except Result := False; end; end;