1 {
2  *****************************************************************************
3   This file is part of the Lazarus Component Library (LCL)
4 
5   See the file COPYING.modifiedLGPL.txt, included in this distribution,
6   for details about the license.
7  *****************************************************************************
8 
9   Author: Mattias Gaertner
10 
11   Abstract:
12     Methods and types for simple HTML help.
13 }
14 unit LazHelpHTML;
15 
16 {$mode objfpc}{$H+}
17 
18 interface
19 
20 uses
21   {$IFDEF MSWindows}Windows, ShellApi,{$ENDIF} // needed for ShellExecute, not good for WinCE, issue #36558
22   Classes, SysUtils,
23   // LazUtils
24   LazFileUtils, UTF8Process, LazUTF8, LazConfigStorage,
25   // LCL
26   LCLProc, LCLIntf, LCLStrConsts, HelpIntfs, LazHelpIntf;
27 
28 type
29   { THTMLHelpDatabase
30 
31     KeywordPrefix: if set, then the database will handle all Keywords
32       beginning with this value. And when the path is created by replacing
33       the prefix with the BaseURL.
34       For example:
35         Put a THTMLHelpDatabase on a form.
36         Set AutoRegister to true.
37         Set KeywordPrefix to 'MyHelp/'
38         Set BaseURL to 'file://'
39 
40         Put a THTMLBrowserHelpViewer on the form.
41         Set AutoRegister to true.
42         Set BrowserPath to '/usr/bin/mozilla'
43 
44         Put a TEdit on a form.
45         Set HelpType to htKeyword
46         Set HelpKeyword to 'MyHelp/page.html'
47 
48         Run the program.
49         Focus the edit field and press F1. The page 'page.html' will be shown.
50         }
51 
52   THTMLHelpDatabase = class(THelpDatabase)
53   private
54     FBaseURL: string;
55     FDefaultBaseURL: string;
56     FKeywordPrefix: string;
57     FKeywordPrefixNode: THelpNode;
IsBaseURLStorednull58     function IsBaseURLStored: boolean;
59     procedure SetBaseURL(const AValue: string);
60     procedure SetBuiltInBaseURL(const AValue: string);
61     procedure SetDefaultBaseURL(const AValue: string);
62   public
63     constructor Create(TheOwner: TComponent); override;
64     destructor Destroy; override;
ShowURLnull65     function ShowURL(const URL, Title: string;
66                      var ErrMsg: string): TShowHelpResult; virtual;
ShowHelpnull67     function ShowHelp(Query: THelpQuery; BaseNode, NewNode: THelpNode;
68                       QueryItem: THelpQueryItem;
69                       var ErrMsg: string): TShowHelpResult; override;
GetNodesForKeywordnull70     function GetNodesForKeyword(const HelpKeyword: string;
71                                 var ListOfNodes: THelpNodeQueryList;
72                                 var ErrMsg: string): TShowHelpResult; override;
GetEffectiveBaseURLnull73     function GetEffectiveBaseURL: string;
74     procedure Load(Storage: TConfigStorage); override;
75     procedure Save(Storage: TConfigStorage); override;
76     property DefaultBaseURL: string read FDefaultBaseURL write SetDefaultBaseURL;// used, if BaseURL is empty
77   published
78     property BuiltInBaseURL: string read FDefaultBaseURL write SetBuiltInBaseURL;// read only, shown in the IDE help options
79     property BaseURL: string read FBaseURL write SetBaseURL stored IsBaseURLStored;
80     property AutoRegister;
81     property KeywordPrefix: string read FKeywordPrefix write FKeywordPrefix;// see above
82   end;
83 
84 
85   { THTMLBrowserHelpViewer
86 
87     If no browser is specified it searches for a common browser. }
88 
89   TOnFindDefaultBrowser = procedure(var DefaultBrowser, Params: string) of object;
90 
91   THTMLBrowserHelpViewer = class(THelpViewer)
92   private
93     FBrowserParams: string;
94     FBrowserPath: string;
95     FDefaultBrowser: string;
96     FDefaultBrowserParams: string;
97     FOnFindDefaultBrowser: TOnFindDefaultBrowser;
98     procedure SetBrowserParams(const AValue: string);
99     procedure SetBrowserPath(const AValue: string);
100   public
101     constructor Create(TheOwner: TComponent); override;
ShowNodenull102     function ShowNode(Node: THelpNode; var ErrMsg: string): TShowHelpResult; override;
103     procedure FindDefaultBrowser(out Browser, Params: string); virtual;
104     procedure Assign(Source: TPersistent); override;
105     procedure Load(Storage: TConfigStorage); override;
106     procedure Save(Storage: TConfigStorage); override;
GetLocalizedNamenull107     function GetLocalizedName: string; override;
108     property OnFindDefaultBrowser: TOnFindDefaultBrowser
109                read FOnFindDefaultBrowser write FOnFindDefaultBrowser;
110   published
111     property BrowserPath: string read FBrowserPath write SetBrowserPath;
112     property BrowserParams: string read FBrowserParams write SetBrowserParams;
113     property AutoRegister;
114   end;
115 
116 
117 procedure Register;
118 
119 implementation
120 
121 procedure Register;
122 begin
123   RegisterComponents('System',[THTMLHelpDatabase,THTMLBrowserHelpViewer]);
124 end;
125 
126 { THTMLHelpDatabase }
127 
128 procedure THTMLHelpDatabase.SetBaseURL(const AValue: string);
129 begin
130   if FBaseURL=AValue then exit;
131   //debugln('THTMLHelpDatabase.SetBaseURL ',dbgsName(Self),' ',AValue);
132   if AValue=DefaultBaseURL then
133     FBaseURL:=''
134   else
135     FBaseURL:=AValue;
136 end;
137 
138 procedure THTMLHelpDatabase.SetBuiltInBaseURL(const AValue: string);
139 begin
140   if AValue=BuiltInBaseURL then exit;
141   raise Exception.Create(rsTheBuiltInURLIsReadOnlyChangeTheBaseURLInstead);
142 end;
143 
144 procedure THTMLHelpDatabase.SetDefaultBaseURL(const AValue: string);
145 begin
146   if FDefaultBaseURL=AValue then exit;
147   if (FBaseURL='') or (FBaseURL=FDefaultBaseURL) then
148     FBaseURL:=FDefaultBaseURL;
149   FDefaultBaseURL:=AValue;
150 end;
151 
THTMLHelpDatabase.IsBaseURLStorednull152 function THTMLHelpDatabase.IsBaseURLStored: boolean;
153 begin
154   Result:=FBaseURL<>DefaultBaseURL;
155 end;
156 
157 constructor THTMLHelpDatabase.Create(TheOwner: TComponent);
158 begin
159   inherited Create(TheOwner);
160   AddSupportedMimeType('text/html');
161 end;
162 
163 destructor THTMLHelpDatabase.Destroy;
164 begin
165   FreeAndNil(FKeywordPrefixNode);
166   inherited Destroy;
167 end;
168 
THTMLHelpDatabase.ShowURLnull169 function THTMLHelpDatabase.ShowURL(const URL, Title: string; var ErrMsg: string
170   ): TShowHelpResult;
171 var
172   URLType, URLPath, URLParams: string;
173   BaseURLType, BaseURLPath, BaseURLParams: string;
174   Viewer: THelpViewer;
175   EffBaseURL: String;
176   Node: THelpNode;
177   FullURL: String;
178 begin
179   //DebugLn('THTMLHelpDatabase.ShowURL A URL="',URL,'" Title="',Title,'"');
180 
181   // find HTML viewer
182   Result:=FindViewer('text/html',ErrMsg,Viewer);
183   if Result<>shrSuccess then exit;
184 
185   // make URL absolute
186   SplitURL(URL,URLType,URLPath,URLParams);
187   //debugln('THTMLHelpDatabase.ShowURL A NewNode.URL=',URL,' URLType=',URLType,' URLPath=',URLPath,' URLParams=',URLParams);
188 
189   if URLType='file' then begin
190     if not URLFilenameIsAbsolute(URLPath) then begin
191       EffBaseURL:=GetEffectiveBaseURL;
192       //DebugLn('THTMLHelpDatabase.ShowURL file relative, making absolute... EffBaseURL="',EffBaseURL,'"');
193       if EffBaseURL<>'' then begin
194         SplitURL(EffBaseURL,BaseURLType,BaseURLPath,BaseURLParams);
195         if (BaseURLPath<>'') then
196           URLPath:=BaseURLPath+URLPath;
197         URLType:=BaseURLType;
198       end;
199     end;
200     if (URLType='file') and (not URLFilenameIsAbsolute(URLPath)) then
201       URLPath:=FilenameToURLPath(TrimFilename(GetCurrentDirUTF8+PathDelim))+URLPath;
202 
203     if (URLType='file') and (not FileExistsUTF8(URLPath)) then begin
204       Result:=shrContextNotFound;
205       ErrMsg:=Format(hhsHelpTheHelpDatabaseWasUnableToFindFile, [ID, URLPath]);
206       exit;
207     end;
208   end;
209   FullURL:=CombineURL(URLType,URLPath,URLParams);
210   {$IFNDEF DisableChecks}
211   debugln('THTMLHelpDatabase.ShowURL B URL=',URL,' URLType=',URLType,' URLPath=',URLPath,' URLParams=',URLParams);
212   {$ENDIF}
213 
214   // call viewer
215   Node:=nil;
216   try
217     Node:=THelpNode.CreateURL(Self,Title,FullURL);
218     Result:=Viewer.ShowNode(Node,ErrMsg);
219   finally
220     Node.Free;
221   end;
222 end;
223 
ShowHelpnull224 function THTMLHelpDatabase.ShowHelp(Query: THelpQuery;
225   BaseNode, NewNode: THelpNode; QueryItem: THelpQueryItem;
226   var ErrMsg: string): TShowHelpResult;
227 begin
228   ErrMsg:='';
229   Result:=shrContextNotFound;
230   if NewNode.URLValid then begin
231     Result:=ShowURL(NewNode.URL,NewNode.Title,ErrMsg);
232   end else begin
233     Result:=shrContextNotFound;
234     ErrMsg:='THTMLHelpDatabase.ShowHelp Node.URLValid=false Node.URL="'+NewNode.URL+'"';
235   end;
236 end;
237 
GetNodesForKeywordnull238 function THTMLHelpDatabase.GetNodesForKeyword(const HelpKeyword: string;
239   var ListOfNodes: THelpNodeQueryList; var ErrMsg: string): TShowHelpResult;
240 var
241   Path: String;
242 begin
243   Result:=inherited GetNodesForKeyword(HelpKeyword, ListOfNodes, ErrMsg);
244   if Result<>shrSuccess then exit;
245 
246   if not (csDesigning in ComponentState)
247   and (KeywordPrefix<>'')
248   and (LeftStr(HelpKeyword,length(KeywordPrefix))=KeywordPrefix) then begin
249     // HelpKeyword starts with KeywordPrefix -> add default node
250     if FKeywordPrefixNode=nil then
251       FKeywordPrefixNode:=THelpNode.CreateURL(Self,'','');
252     Path:=copy(HelpKeyword,length(KeywordPrefix)+1,length(HelpKeyword));
253     FKeywordPrefixNode.Title:='Show page '+Path;
254     FKeywordPrefixNode.URL:='file://'+Path;
255     CreateNodeQueryListAndAdd(FKeywordPrefixNode,nil,ListOfNodes,true);
256   end;
257 end;
258 
GetEffectiveBaseURLnull259 function THTMLHelpDatabase.GetEffectiveBaseURL: string;
260 begin
261   Result:='';
262   if BaseURL<>'' then begin
263     Result:=BaseURL;
264     if (Databases<>nil) then begin
265       Databases.SubstituteMacros(Result);
266       Result:=FilenameToURLPath(Result);
267     end;
268     //debugln('THTMLHelpDatabase.GetEffectiveBaseURL using BaseURL="',Result,'"');
269   end else if (BasePathObject<>nil) and (Databases<>nil) then begin
270     Result:=Databases.GetBaseURLForBasePathObject(BasePathObject);
271     //debugln('THTMLHelpDatabase.GetEffectiveBaseURL using BasePathObject="',Result,'"');
272   end;
273   if (Result='') and (DefaultBaseURL<>'') then begin
274     Result:=DefaultBaseURL;
275     if (Databases<>nil) then begin
276       Databases.SubstituteMacros(Result);
277       Result:=FilenameToURLPath(Result);
278     end;
279     //debugln('THTMLHelpDatabase.GetEffectiveBaseURL using DefaultBaseURL="',Result,'"');
280   end;
281   Result:=AppendURLPathDelim(Result);
282 end;
283 
284 procedure THTMLHelpDatabase.Load(Storage: TConfigStorage);
285 begin
286   inherited Load(Storage);
287   BaseURL:=Storage.GetValue('BaseURL/Value',DefaultBaseURL);
288 end;
289 
290 procedure THTMLHelpDatabase.Save(Storage: TConfigStorage);
291 begin
292   inherited Save(Storage);
293   Storage.SetDeleteValue('BaseURL/Value',BaseURL,DefaultBaseURL);
294 end;
295 
296 { THTMLBrowserHelpViewer }
297 
298 procedure THTMLBrowserHelpViewer.SetBrowserParams(const AValue: string);
299 begin
300   if FBrowserParams=AValue then exit;
301   FBrowserParams:=AValue;
302 end;
303 
304 procedure THTMLBrowserHelpViewer.SetBrowserPath(const AValue: string);
305 begin
306   if FBrowserPath=AValue then exit;
307   FBrowserPath:=AValue;
308 end;
309 
310 constructor THTMLBrowserHelpViewer.Create(TheOwner: TComponent);
311 begin
312   inherited Create(TheOwner);
313   AddSupportedMimeType('text/html');
314   FBrowserParams:='%s';
315   ParameterHelp:=hhsHelpTheMacroSInBrowserParamsWillBeReplacedByTheURL;
316 end;
317 
THTMLBrowserHelpViewer.ShowNodenull318 function THTMLBrowserHelpViewer.ShowNode(Node: THelpNode; var ErrMsg: string
319   ): TShowHelpResult;
320 var
321   URLMacroPos: LongInt;
322   BrowserProcess: TProcessUTF8;
323   Executable, ParamsStr: String;
324   IsShellStr: Boolean = false;
325 begin
326   Result:=shrViewerError;
327   ErrMsg:='';
328   if (not Node.URLValid) then begin
329     ErrMsg:='THTMLBrowserHelpViewer.ShowNode Node.URLValid=false';
330     exit;
331   end;
332   if (Node.URL='') then begin
333     ErrMsg:='THTMLBrowserHelpViewer.ShowNode Node.URL empty';
334     exit;
335   end;
336 
337   // check browser path
338   Executable:=BrowserPath;
339   ParamsStr:=BrowserParams;
340   if Executable='' then
341     FindDefaultBrowser(Executable, ParamsStr);
342   if Executable='' then begin
343     if (HelpDatabases<>nil)
344     and (CompareText(HelpDatabases.ClassName,'TIDEHelpDatabases')=0) then
345       ErrMsg:=Format(hhsHelpNoHTMLBrowserFoundPleaseDefineOne,[LineEnding])
346     else
347       ErrMsg:=hhsHelpNoHTMLBrowserFound;
348     exit;
349   end;
350   {$ifdef windows}
351   //The result of FindDefaultBrowser may or may not be quoted on Windows
352   //Since on Windows, a filename cannot contain a double quote, we simply remove them
353   //otherwise FileExistsUf8 and FileIsExecutable fail. Issue #0030502
354   if (Length(Executable) > 1) and (Executable[1] = '"') and (Executable[Length(Executable)] = '"') then
355     Executable := Copy(Executable, 2, Length(Executable)-2);
356   // Preparation of special handling for Microsoft Edge in Win10, issue #35659
357   IsShellStr := UpperCase(LeftStr(Executable,Pos(':',Executable)))='SHELL:';
358   {$endif windows}
359   if not IsShellStr then begin
360     if (not FileExistsUTF8(Executable)) then begin
361       ErrMsg:=Format(hhsHelpBrowserNotFound, [Executable]);
362       exit;
363     end;
364     if (not FileIsExecutable(Executable)) then begin
365       ErrMsg:=Format(hhsHelpBrowserNotExecutable, [Executable]);
366       exit;
367     end;
368   end;
369   //debugln('THTMLBrowserHelpViewer.ShowNode Node.URL=',Node.URL);
370 
371   // create params and replace %ParamsStr for URL
372   URLMacroPos:=Pos('%s',ParamsStr);
373   if URLMacroPos>=1 then
374     ReplaceSubstring(ParamsStr,URLMacroPos,2,Node.URL)
375   else begin
376     if ParamsStr<>'' then
377       ParamsStr:=ParamsStr+' ';
378     ParamsStr:=ParamsStr+Node.URL;
379   end;
380 
381   {$IFNDEF DisableChecks}
382   debugln('THTMLBrowserHelpViewer.ShowNode Executable="',Executable,'" Params="',ParamsStr,'"');
383   {$ENDIF}
384 
385   // run
386   {$IFDEF MSWindows}     // not good for WinCE! Issue #36558.
387   // Special handling for Microsoft Edge in Win10, issue #35659
388   if IsShellStr then begin
389     if ShellExecute(0,'open',PChar(Executable),PChar(ParamsStr),'',SW_SHOWNORMAL)<=32 then
390       ErrMsg := Format(hhsHelpErrorWhileExecuting,[Executable+' ',ParamsStr, LineEnding, 'ShellExecute'])
391     else
392       Result := shrSuccess;
393   end else
394   {$ENDIF}
395   try
396     BrowserProcess:=TProcessUTF8.Create(nil);
397     try
398       BrowserProcess.InheritHandles:=false;
399       BrowserProcess.Executable:=Executable;
400       SplitCmdLineParams(ParamsStr,BrowserProcess.Parameters);
401       BrowserProcess.Execute;
402     finally
403       BrowserProcess.Free;
404     end;
405     Result:=shrSuccess;
406   except
407     on E: Exception do begin
408       ErrMsg:=Format(hhsHelpErrorWhileExecuting, [Executable+' '+ParamsStr, LineEnding, E.Message]);
409     end;
410   end;
411 end;
412 
413 procedure THTMLBrowserHelpViewer.FindDefaultBrowser(out Browser, Params: string);
414 begin
415   if FDefaultBrowser='' then
416   begin
417     if Assigned(OnFindDefaultBrowser) then
418       OnFindDefaultBrowser(FDefaultBrowser, FDefaultBrowserParams);
419   end;
420   if FDefaultBrowser = '' then
421     LCLIntf.FindDefaultBrowser(FDefaultBrowser, FDefaultBrowserParams);
422 
423   Browser := FDefaultBrowser;
424   Params := FDefaultBrowserParams;
425 
426   //DebugLn('THTMLBrowserHelpViewer.FindDefaultBrowser Browser=',Browser,' Params=',Params);
427 end;
428 
429 procedure THTMLBrowserHelpViewer.Assign(Source: TPersistent);
430 var
431   Viewer: THTMLBrowserHelpViewer;
432 begin
433   if Source is THTMLBrowserHelpViewer then begin
434     Viewer:=THTMLBrowserHelpViewer(Source);
435     BrowserPath:=Viewer.BrowserPath;
436     BrowserParams:=Viewer.BrowserParams;
437   end;
438   inherited Assign(Source);
439 end;
440 
441 procedure THTMLBrowserHelpViewer.Load(Storage: TConfigStorage);
442 begin
443   BrowserPath:=Storage.GetValue('Browser/Path','');
444   BrowserParams:=Storage.GetValue('Browser/Params','%s');
445 end;
446 
447 procedure THTMLBrowserHelpViewer.Save(Storage: TConfigStorage);
448 begin
449   Storage.SetDeleteValue('Browser/Path',BrowserPath,'');
450   Storage.SetDeleteValue('Browser/Params',BrowserParams,'%s');
451 end;
452 
THTMLBrowserHelpViewer.GetLocalizedNamenull453 function THTMLBrowserHelpViewer.GetLocalizedName: string;
454 begin
455   Result:='HTML Browser';
456 end;
457 
458 end.
459 
460