spiegel-keyman/common/windows/delphi/general/utilhttp.pas
Marc Durdin 6eefb3a771 fix(developer): EncodeURL was not handling spaces
Fixes #7810.

This addresses a regression introduced in #7631, where URL parameters
with spaces would be encoded into `+` instead of `%20`. Looking a bit
deeper at the Delphi `TNetEncoding.URL.Encode` function I realised that
it was entirely inadequate. Some guy named Marc Durdin wrote a blog a
good few years ago about the problem, and that's what I ended up using.

This encoding issue caused filenames with spaces (by default, project
paths in Developer have spaces) to give a 404 when editing a touch
layout, which meant that the touch keyboards could not be saved.

Also fixes KEYMAN-DEVELOPER-74, where the + encoding caused multiple
entries to appear in the filename cache.

Amusing to Google this problem, find solid answer on SO, which pointed
to my very own blog. Embarrassing that my own code didn't already
include my own fix.

A secondary issue is also fixed here, where request parameters were
double-decoded for formencoded POST requests. The fix for broken URL
encodings was only required for GET requests.

This also showed up in KEYMAN-DEVELOPER-74, with double-encoded paths
being registered as source files.
2022-11-26 07:09:53 +07:00

198 lines
5 KiB
ObjectPascal

(*
Name: utilhttp
Copyright: Copyright (C) SIL International.
Documentation:
Description:
Create Date: 1 Aug 2006
Modified Date: 28 Aug 2014
Authors: mcdurdin
Related Files:
Dependencies:
Bugs:
Todo:
Notes:
History: 01 Aug 2006 - mcdurdin - Refactor util functions into multiple units
23 Aug 2006 - mcdurdin - Use Unicode strings
06 Oct 2006 - mcdurdin - Add GetParamsFromURL function
25 May 2010 - mcdurdin - I1632 - Keyboard Options - form posting support for keyboard option pages
04 May 2012 - mcdurdin - I3306 - V9.0 - Remove TntControls + Win9x support
04 May 2012 - mcdurdin - I3308 - V9.0 - Start to move towards Delphi namespaces
04 May 2012 - mcdurdin - I3309 - V9.0 - Migrate to Delphi XE2, VS2010, svn 1.7
28 Aug 2014 - mcdurdin - I4390 - V9.0 - Free vs Pro
*)
unit utilhttp; // I3306 // I3308 // I3309
interface
uses
System.Classes,
System.SysUtils,
System.WideStrUtils,
System.NetEncoding;
function GetParamsFromURL(URL: WideString; var Params: TStringList): Boolean;
function GetParamsFromURLEx(URL: WideString; var Params: TStringList): Boolean;
procedure DecodeAndSetParams(const AValue: WideString; Params: TStringList);
function URLDecode(ASrc: ansistring): string; // I4390
function URLEncode(Asrc: string): string;
function ConvertFileURLToPath(s: WideString): WideString;
function ConvertPathToFileURL(s: WideString): WideString;
implementation
// Source:
// https://marc.durdin.net/2015/08/an-update-for-encodeuricomponent/
// Stack Overflow:
// https://stackoverflow.com/questions/776302/standard-url-encode-function
function URLEncode(ASrc: string): string;
const
HexMap: string = '0123456789ABCDEF';
function IsSafeChar(ch: Byte): Boolean;
begin
if (ch >= 48) and (ch <= 57) then Result := True // 0-9
else if (ch >= 65) and (ch <= 90) then Result := True // A-Z
else if (ch >= 97) and (ch <= 122) then Result := True // a-z
else if (ch = 33) then Result := True // !
else if (ch >= 39) and (ch <= 42) then Result := True // '()*
else if (ch >= 45) and (ch <= 46) then Result := True // -.
else if (ch = 95) then Result := True // _
else if (ch = 126) then Result := True // ~
else Result := False;
end;
var
I, J: Integer;
Bytes: TBytes;
begin
Result := '';
Bytes := TEncoding.UTF8.GetBytes(ASrc);
I := 0;
J := Low(Result);
SetLength(Result, Length(Bytes) * 3); // space to %xx encode every byte
while I < Length(Bytes) do
begin
if IsSafeChar(Bytes[I]) then
begin
Result[J] := Char(Bytes[I]);
Inc(J);
end
else
begin
Result[J] := '%';
Result[J+1] := HexMap[(Bytes[I] shr 4) + Low(HexMap)];
Result[J+2] := HexMap[(Bytes[I] and 15) + Low(HexMap)];
Inc(J,3);
end;
Inc(I);
end;
SetLength(Result, J-Low(Result));
end;
function URLDecode(ASrc: AnsiString): string;
begin
Result := TNetEncoding.URL.Decode(string(ASrc));
end;
procedure DecodeAndSetParams(const AValue: WideString; Params: TStringList);
var
i, j : Integer;
s: WideString;
begin
// Convert special characters
// ampersand '&' separates values {Do not Localize}
Params.BeginUpdate;
try
Params.Clear;
i := 1;
while i <= length(AValue) do
begin
j := i;
while (j <= length(AValue)) and (AValue[j] <> '&') do
begin
inc(j);
end;
s := copy(AValue, i, j-i);
// See RFC 1866 section 8.2.1. TP
s := StringReplace(s, '+', ' ', [rfReplaceAll]); {do not localize}
Params.Add(URLDecode(AnsiString(s)));
i := j + 1;
end;
finally
Params.EndUpdate;
end;
end;
function GetParamsFromURL(URL: WideString; var Params: TStringList): Boolean;
var
Command: WideString;
s: WideString;
n: Integer;
begin
s := URL;
if Copy(s,1,7) = 'keyman:' then
begin
params := TStringList.Create;
Delete(s,1,7);
n := Pos('?',s);
if n > 0 then
begin
Command := Copy(s,1,n-1);
DecodeAndSetParams(Copy(s,n+1,Length(s)), params);
end
else
Command := s;
params.Insert(0, command);
Result := True;
end
else
Result := False;
end;
function GetParamsFromURLEx(URL: WideString; var Params: TStringList): Boolean;
var
Command: WideString;
s: WideString;
n: Integer;
begin
s := URL;
params := TStringList.Create;
Delete(s,1,7);
n := Pos('?',s);
if n > 0 then
begin
Command := Copy(s,1,n-1);
DecodeAndSetParams(Copy(s,n+1,Length(s)), params);
end
else
Command := s;
params.Insert(0, URL);
params.Insert(0, command);
Result := True;
end;
function ConvertFileURLToPath(s: WideString): WideString;
begin
s := URLDecode(AnsiString(s));
if Copy(s, 1, 8) = 'file:///'
then Result := WideReplaceText(Copy(s, 9, MAXINT), '/', '\')
else Result := s;
end;
function ConvertPathToFileURL(s: WideString): WideString;
begin
Result := 'file:///'+UrlEncode(WideReplaceText(s, '\', '/'));
end;
end.