mirror of
https://github.com/keymanapp/keyman.git
synced 2026-08-12 20:05:34 +00:00
Also updates the only other place where a project file is loaded like this, in the project renderer, and handles it too (this scenario is less likely to happen because the project file must already have been loaded in order to be presented in the UI). Fixes: #11557 Fixes: KEYMAN-DEVELOPER-1Z2
354 lines
9.8 KiB
ObjectPascal
354 lines
9.8 KiB
ObjectPascal
unit Keyman.Developer.System.HttpServer.App;
|
|
|
|
interface
|
|
|
|
uses
|
|
IdContext,
|
|
IdCustomHTTPServer,
|
|
IdHTTPServer,
|
|
|
|
Keyman.System.HttpServer.Base,
|
|
|
|
utilfiletypes;
|
|
|
|
// /app
|
|
|
|
type
|
|
TAppHttpResponder = class(TBaseHttpResponder)
|
|
private
|
|
FAppRoot: string;
|
|
FStandardTemplatePath: string;
|
|
procedure RespondProject(doc: string; AContext: TIdContext;
|
|
ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo);
|
|
procedure RespondHelp(doc: string; AContext: TIdContext;
|
|
ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo);
|
|
procedure RespondSettings(doc: string; AContext: TIdContext;
|
|
ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo);
|
|
public
|
|
constructor Create;
|
|
procedure ProcessRequest(AContext: TIdContext;
|
|
ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo);
|
|
end;
|
|
|
|
implementation
|
|
|
|
uses
|
|
System.Classes,
|
|
System.JSON,
|
|
System.SysUtils,
|
|
System.Variants,
|
|
Winapi.Windows,
|
|
Xml.XMLDoc,
|
|
Xml.XMLIntf,
|
|
|
|
JsonUtil,
|
|
KeymanDeveloperOptions,
|
|
Keyman.Developer.System.Project.Project,
|
|
Keyman.Developer.System.Project.ProjectFile,
|
|
Keyman.Developer.System.Project.ProjectLoader,
|
|
Keyman.Developer.System.Project.WelcomeRenderer,
|
|
RedistFiles;
|
|
|
|
{ TAppHttpServer }
|
|
|
|
procedure TAppHttpResponder.RespondHelp(doc: string; AContext: TIdContext;
|
|
ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo);
|
|
var
|
|
xml, xsl: IXMLDocument;
|
|
s: WideString;
|
|
begin
|
|
|
|
if FStandardTemplatePath = '' then
|
|
begin
|
|
FStandardTemplatePath := GetXMLTemplatePath + 'help\';
|
|
end;
|
|
|
|
if (doc <> 'help/') and (doc <> 'help/index') then
|
|
begin
|
|
Respond404(AContext, ARequestInfo, AResponseInfo);
|
|
Exit;
|
|
end;
|
|
|
|
xml := TXMLDocument.Create(nil);
|
|
xml.ParseOptions := [poResolveExternals];
|
|
// I902 - resolve externals when loading XML files so locale.xml parses
|
|
xml.LoadFromFile(FStandardTemplatePath + 'contexthelp.xml');
|
|
|
|
xsl := TXMLDocument.Create(nil);
|
|
// xml.ParseOptions := [poResolveExternals]; // I902 - resolve externals when loading XML files so locale.xml parses
|
|
|
|
xsl.LoadFromFile(FStandardTemplatePath + 'help.xsl');
|
|
xml.Node.transformNode(xsl.Node, s);
|
|
|
|
AResponseInfo.ContentType := 'text/html; charset=UTF-8';
|
|
AResponseInfo.ContentText := s;
|
|
end;
|
|
|
|
procedure TAppHttpResponder.RespondProject(doc: string; AContext: TIdContext;
|
|
ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo);
|
|
|
|
procedure RespondWelcome;
|
|
begin
|
|
AResponseInfo.ContentType := 'text/html; charset=UTF-8';
|
|
AResponseInfo.ContentText := TWelcomeRenderer.Render;
|
|
end;
|
|
|
|
procedure RespondProjectFile;
|
|
var
|
|
path: string;
|
|
p: TProject;
|
|
begin
|
|
if ARequestInfo.Params.IndexOfName('path') < 0 then
|
|
begin
|
|
AResponseInfo.ResponseNo := 400;
|
|
AResponseInfo.ResponseText := 'Missing parameter path';
|
|
Exit;
|
|
end;
|
|
|
|
path := CrackUTF8ZeroExtendedString(ARequestInfo.CommandType, ARequestInfo.Params.Values['path']);
|
|
|
|
if (Path <> '') and (not DirectoryExists(ExtractFileDir(path)) or not SameText(ExtractFileExt(path), Ext_ProjectSource)) then
|
|
begin
|
|
AResponseInfo.ResponseNo := 404;
|
|
AResponseInfo.ResponseText := 'Project file '+path+' does not exist.';
|
|
Exit;
|
|
end;
|
|
|
|
// Transform the .kpj
|
|
try
|
|
p := TProject.Create(ptUnknown, path, True);
|
|
except
|
|
on E:EProjectLoader do
|
|
begin
|
|
AResponseInfo.ResponseNo := 400;
|
|
AResponseInfo.ResponseText := 'Invalid project file: '+E.Message;
|
|
Exit;
|
|
end;
|
|
end;
|
|
try
|
|
AResponseInfo.ContentType := 'text/html; charset=UTF-8';
|
|
AResponseInfo.ContentText := p.Render;
|
|
finally
|
|
p.Free;
|
|
end;
|
|
end;
|
|
|
|
procedure RespondIco;
|
|
var
|
|
path: string;
|
|
begin
|
|
if ARequestInfo.Params.IndexOfName('path') < 0 then
|
|
begin
|
|
AResponseInfo.ResponseNo := 400;
|
|
AResponseInfo.ResponseText := 'Missing parameter path';
|
|
Exit;
|
|
end;
|
|
|
|
path := CrackUTF8ZeroExtendedString(ARequestInfo.CommandType, ARequestInfo.Params.Values['path']);
|
|
|
|
if not FileExists(path) or (
|
|
not SameText(ExtractFileExt(path), '.ico') and
|
|
not SameText(ExtractFileExt(path), '.bmp')
|
|
) then
|
|
begin
|
|
AResponseInfo.ResponseNo := 404;
|
|
AResponseInfo.ResponseText := 'Image file '+path+' does not exist.';
|
|
Exit;
|
|
end;
|
|
|
|
RespondFile(path, AContext, ARequestInfo, AResponseInfo);
|
|
end;
|
|
|
|
procedure RespondResource;
|
|
begin
|
|
Delete(doc, 1, 4);
|
|
if (Pos('/', doc) > 0) or (Pos('\', doc) > 0) then
|
|
Respond404(AContext, ARequestInfo, AResponseInfo)
|
|
else
|
|
RespondFile(TProject.StandardTemplatePath + '/' + doc, AContext, ARequestInfo, AResponseInfo);
|
|
end;
|
|
|
|
procedure RespondState;
|
|
var
|
|
displayState, path: string;
|
|
xmldoc: IXMLDocument;
|
|
FNewDisplayState: PChar;
|
|
FNewPath: PChar;
|
|
begin
|
|
with TStringStream.Create('', TEncoding.UTF8) do
|
|
try
|
|
CopyFrom(ARequestInfo.PostStream, 0);
|
|
displayState := DataString;
|
|
finally
|
|
Free;
|
|
end;
|
|
|
|
try
|
|
xmldoc := LoadXMLData(displayState);
|
|
except
|
|
on E:Exception do
|
|
begin
|
|
Respond404(AContext, ARequestInfo, AResponseInfo);
|
|
Exit;
|
|
end;
|
|
end;
|
|
|
|
try
|
|
if (xmldoc.DocumentElement.ChildNodes.IndexOf('path') >= 0) and not
|
|
VarIsNull(xmldoc.DocumentElement.ChildValues['path']) then
|
|
path := xmldoc.DocumentElement.ChildValues['path'];
|
|
except
|
|
path := '';
|
|
end;
|
|
|
|
// Saving state
|
|
|
|
if (Path <> '') and (not DirectoryExists(ExtractFileDir(path)) or not SameText(ExtractFileExt(path), Ext_ProjectSource)) then
|
|
begin
|
|
AResponseInfo.ResponseNo := 404;
|
|
AResponseInfo.ResponseText := 'Project file '+path+' does not exist.';
|
|
Exit;
|
|
end;
|
|
|
|
if GlobalProjectStateWndHandle <> 0 then
|
|
begin
|
|
// Potential for a race condition here as the window
|
|
// may be destroyed before we post to it. Can only happen at
|
|
// process destruction time and really not worth worrying about.
|
|
FNewDisplayState := StrNew(PChar(displayState));
|
|
FNewPath := StrNew(PChar(Path));
|
|
PostMessage(GlobalProjectStateWndHandle, WM_USER_ProjectUpdateDisplayState, NativeInt(FNewPath), NativeInt(FNewDisplayState));
|
|
end;
|
|
end;
|
|
begin
|
|
//
|
|
// http://localhost:8008/app/project/(index)?path=<fqp-to-.kpj>
|
|
// http://localhost:8008/app/project/<resource>
|
|
//
|
|
Delete(doc, 1, 8);
|
|
if (doc = '') or (doc = 'index') then
|
|
RespondProjectFile
|
|
else if doc = 'welcome' then
|
|
RespondWelcome
|
|
else if doc = 'ico' then
|
|
RespondIco
|
|
else if Copy(doc, 1, 4) = 'res/' then
|
|
RespondResource
|
|
else if doc = 'state' then
|
|
RespondState;
|
|
end;
|
|
|
|
procedure TAppHttpResponder.RespondSettings(doc: string; AContext: TIdContext;
|
|
ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo);
|
|
|
|
procedure RespondEditorSettings;
|
|
var
|
|
root: TJSONObject;
|
|
offset: Integer;
|
|
theme: TJSONObject;
|
|
begin
|
|
// Respond as JSON
|
|
|
|
root := TJSONObject.Create;
|
|
try
|
|
// Basic settings - tabs
|
|
|
|
// TODO: FKeymanDeveloperOptins is technically not thread safe.
|
|
root.AddPair('useTabChar', TJSONBool.Create(FKeymanDeveloperOptions.UseTabChar));
|
|
root.AddPair('indentSize', TJSONNumber.Create(FKeymanDeveloperOptions.IndentSize));
|
|
|
|
// Specify a theme by name or as a JSON object, loaded from a custom theme file
|
|
|
|
if FKeymanDeveloperOptions.EditorTheme <> '' then
|
|
begin
|
|
if TKeymanDeveloperOptions.IsDefaultEditorTheme(FKeymanDeveloperOptions.EditorTheme) then
|
|
begin
|
|
root.AddPair('theme', FKeymanDeveloperOptions.EditorTheme);
|
|
end
|
|
else
|
|
begin
|
|
if FileExists(FKeymanDeveloperOptions.EditorTheme) then
|
|
begin
|
|
try
|
|
theme := LoadJSONFromFile(FKeymanDeveloperOptions.EditorTheme, offset);
|
|
if theme <> nil then
|
|
root.AddPair('theme', theme);
|
|
except
|
|
on E:Exception do ; // We will ignore file and json errors and use default theme
|
|
end;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
AResponseInfo.ContentType := 'application/json';
|
|
AResponseInfo.CharSet := 'UTF-8';
|
|
AResponseInfo.ContentText := JSONToString(root);
|
|
finally
|
|
root.Free;
|
|
end;
|
|
end;
|
|
|
|
begin
|
|
if doc = 'settings/editor' then
|
|
begin
|
|
RespondEditorSettings;
|
|
end
|
|
else
|
|
Respond404(AContext, ARequestInfo, AResponseInfo);
|
|
end;
|
|
|
|
constructor TAppHttpResponder.Create;
|
|
begin
|
|
FAppRoot := GetXMLTemplatePath + 'app\';
|
|
end;
|
|
|
|
procedure TAppHttpResponder.ProcessRequest(AContext: TIdContext;
|
|
ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo);
|
|
var
|
|
doc: string;
|
|
begin
|
|
// We know the request starts with /app/
|
|
doc := ARequestInfo.Document;
|
|
Delete(doc, 1, 5); // /app/
|
|
|
|
// App requests are only accepted from local computer
|
|
// So test that the request comes from localhost
|
|
if (AContext.Binding.PeerIP <> '127.0.0.1') and
|
|
(AContext.Binding.PeerIP <> '0:0:0:0:0:0:0:1') and
|
|
not doc.StartsWith('lib/sentry/') then // We'll allow sentry for non-local requests
|
|
begin
|
|
AResponseInfo.ResponseNo := 403;
|
|
AResponseInfo.ResponseText := 'Access denied';
|
|
Exit;
|
|
end;
|
|
|
|
if Copy(doc, 1, 8) = 'project/' then
|
|
begin
|
|
RespondProject(doc, AContext, ARequestInfo, AResponseInfo);
|
|
Exit;
|
|
end
|
|
else if Copy(doc, 1, 5) = 'help/' then
|
|
begin
|
|
RespondHelp(doc, AContext, ARequestInfo, AResponseInfo);
|
|
Exit;
|
|
end
|
|
else if Copy(doc, 1, 9) = 'settings/' then
|
|
begin
|
|
RespondSettings(doc, AContext, ARequestInfo, AResponseInfo);
|
|
Exit;
|
|
end
|
|
else
|
|
begin
|
|
if IncludesParentFolderReference(doc) then
|
|
begin
|
|
// Block paths that attempt to break out of our 'root'
|
|
Respond404(AContext, ARequestInfo, AResponseInfo);
|
|
end
|
|
else
|
|
RespondFile(FAppRoot + doc, AContext, ARequestInfo, AResponseInfo);
|
|
end;
|
|
|
|
Respond404(AContext, ARequestInfo, AResponseInfo);
|
|
end;
|
|
|
|
end.
|