httpsearcher.pp 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493
  1. unit httpsearcher;
  2. // You can remove the support you do not need.
  3. {$DEFINE USEFIREBIRD}
  4. {$DEFINE USESQLITE}
  5. {$DEFINE USEPOSTGRES}
  6. {$mode objfpc}{$H+}
  7. {$IFDEF USEFIREBIRD}
  8. {$DEFINE USESQLDB}
  9. {$ENDIF}
  10. {$IFDEF USEPOSTGRES}
  11. {$DEFINE USESQLDB}
  12. {$ENDIF}
  13. interface
  14. uses
  15. Classes, SysUtils, DateUtils, sqldb,
  16. {$IFDEF USESQLDB}
  17. SQLDBindexDB,
  18. {$ENDIF}
  19. {$IFDEF USEFIREBIRD}
  20. FBindexDB, // Firebird support
  21. {$ENDIF}
  22. {$IFDEF USESQLITE}
  23. sqliteindexdb, // sqlite 3 support
  24. {$ENDIF}
  25. {$IFDEF USEPOSTGRES}
  26. pgindexdb, // Postgres support
  27. {$ENDIF}
  28. memindexdb, // Custom Memory file. Always enabled
  29. fpIndexer, inifiles, httpdefs, fpjson;
  30. Type
  31. { THTTPSearcher }
  32. THTTPSearcher = Class(TComponent)
  33. private
  34. FAllowCors: Boolean;
  35. FDB : TCustomIndexDB;
  36. FSearch : TFPSearch;
  37. FDefaultMinRank : Integer;
  38. FMinRank : Integer;
  39. FFormattedJSON : Boolean;
  40. FDefaultMetadata,
  41. FIncludeMetaData : Boolean;
  42. FDefaultAvailable : TAvailableMatch;
  43. FMetadata : TJSONObject;
  44. FWordsMetadata : TJSONObject;
  45. procedure ConfigSearch(aRequest: TRequest; aResponse: TResponse);
  46. procedure ConfigWordList(aRequest: TRequest; out aContaining : UTF8string; Out Partial : TAvailableMatch; Out aSimple : Boolean);
  47. function SearchDataToJSON(aID: Integer; const aRes: TSearchWordData): TJSONObject;
  48. procedure SendJSON(J: TJSONObject; aResponse: TResponse);
  49. procedure SetupMetadata;
  50. Protected
  51. function InitSearch(aResponse: TResponse): Boolean;
  52. function SetupDB(aIni: TCustomIniFile): TCustomIndexDB;
  53. Property DB : TCustomIndexDB Read FDB;
  54. Property Search : TFPSearch Read FSearch;
  55. Property MinRank : Integer Read FMinRank;
  56. Property FormattedJSON : Boolean Read FFormattedJSON;
  57. Property AllowCors : Boolean Read FAllowCors;
  58. Public
  59. Function CheckParams(aRequest : TRequest; aResponse : TResponse) : Boolean;
  60. Function CheckSearchParams(aRequest : TRequest; aResponse : TResponse) : Boolean;
  61. Procedure HTMLSearch(aRequest : TRequest; aResponse : TResponse);
  62. Procedure WordList(aRequest : TRequest; aResponse : TResponse);
  63. end;
  64. implementation
  65. function THTTPSearcher.SetupDB(aini :TCustomIniFile) : TCustomIndexDB;
  66. Const
  67. SDatabase = 'Database';
  68. KeyType = 'Type';
  69. KeyDatabaseName = 'DatabaseName';
  70. {$IFDEF USESQLDB}
  71. KeyHostName = 'HostName';
  72. KeyUser = 'User';
  73. KeyPassword = 'Password';
  74. {$ENDIF}
  75. {$IFDEF USESQLDB}
  76. Procedure ConfigSQLDB(DB : TSQLDBIndexDB);
  77. begin
  78. DB.HostName:= aIni.ReadString(SDatabase,KeyHostName,DB.HostName);
  79. DB.DatabasePath := aIni.ReadString(SDatabase,KeyDatabaseName,DB.DatabasePath);
  80. DB.UserName := aIni.ReadString(SDatabase,KeyUser,DB.UserName);
  81. DB.Password := aIni.ReadString(SDatabase,KeyPassword,DB.Password);
  82. end;
  83. {$ENDIF USESQLDB}
  84. {$IFDEF USESQLLITE}
  85. Procedure ConfigSQLIte(SDB : TSQLiteIndexDB);
  86. begin
  87. SDB.FileName := aIni.ReadString(SDatabase,KeyDatabaseName,SDB.FileName);
  88. end;
  89. {$ENDIF}
  90. Procedure ConfigFile(FDB : TFileIndexDB);
  91. begin
  92. FDB.FileName := aIni.ReadString(SDatabase,KeyDatabaseName,FDB.FileName);
  93. end;
  94. Var
  95. {$IFDEF USESQLDB}
  96. QDB : TSQLDBIndexDB;
  97. {$ENDIF}
  98. {$IFDEF USESQLLITE}
  99. SDB : TSQLiteIndexDB;
  100. {$ENDIF}
  101. MDB : TFileIndexDB;
  102. aType : String;
  103. begin
  104. Result:=nil;
  105. aType:=aIni.ReadString(SDatabase,KeyType,'PostGres');
  106. Case lowercase(aType) of
  107. {$IFDEF USEPOSTGRES}
  108. 'postgres' :
  109. begin
  110. QDB := TPGIndexDB.Create(nil);
  111. ConfigSQLDB(QDB);
  112. Result:=QDB;
  113. end;
  114. {$ENDIF}
  115. {$IFDEF USEFIREBIRD}
  116. 'firebird' :
  117. begin
  118. QDB := TFBIndexDB.Create(nil);
  119. ConfigSQLDB(QDB);
  120. Result:=QDB;
  121. end;
  122. {$ENDIF}
  123. {$IFDEF USESQLITE}
  124. 'sqlite' :
  125. begin
  126. SDB := TSQLiteIndexDB.Create(nil);
  127. ConfigSQLite(SDB);
  128. Result:=SDB;
  129. end;
  130. {$ENDIF}
  131. 'file' :
  132. begin
  133. MDB := TFileIndexDB.Create(nil);
  134. ConfigFile(MDB);
  135. Result:=MDB;
  136. end;
  137. else
  138. Raise Exception.CreateFmt('Unknown database type: "%s" ',[aType]);
  139. end;
  140. end;
  141. function THTTPSearcher.CheckParams(aRequest: TRequest; aResponse: TResponse): Boolean;
  142. Var
  143. S : String;
  144. B : Boolean;
  145. begin
  146. S:=aRequest.QueryFields.Values['q'];
  147. Result:=S<>'';
  148. if not Result then
  149. begin
  150. aResponse.Code:=400;
  151. aResponse.CodeText:='Missing q param';
  152. aResponse.SendResponse;
  153. end;
  154. S:=aRequest.QueryFields.Values['r'];
  155. Result:=(S='') or (StrToIntDef(S,-1)<>-1);
  156. if not Result then
  157. begin
  158. aResponse.Code:=400;
  159. aResponse.CodeText:='Wrong value for r';
  160. aResponse.SendResponse;
  161. end;
  162. S:=aRequest.QueryFields.Values['c'];
  163. Result:=(S='') or TryStrToBool(S,B);
  164. if not Result then
  165. begin
  166. aResponse.Code:=400;
  167. aResponse.CodeText:='Wrong value for c';
  168. aResponse.SendResponse;
  169. end;
  170. S:=aRequest.QueryFields.Values['m'];
  171. Result:=(S='') or TryStrToBool(S,B);
  172. if not Result then
  173. begin
  174. aResponse.Code:=400;
  175. aResponse.CodeText:='Wrong value for m';
  176. aResponse.SendResponse;
  177. end;
  178. end;
  179. function THTTPSearcher.CheckSearchParams(aRequest: TRequest; aResponse: TResponse): Boolean;
  180. Var
  181. m,S : String;
  182. B : Boolean;
  183. begin
  184. S:=aRequest.QueryFields.Values['q'];
  185. M:=aRequest.QueryFields.Values['t'];
  186. Result:=(M='');
  187. if not Result then
  188. case lowercase(M) of
  189. 'all' :
  190. if S<>'' then
  191. begin
  192. aResponse.Code:=400;
  193. aResponse.CodeText:='Q must be empty';
  194. aResponse.SendResponse;
  195. end;
  196. 'contains',
  197. 'exact',
  198. 'startswith' :
  199. if S='' then
  200. begin
  201. aResponse.Code:=400;
  202. aResponse.CodeText:='Q may not be empty';
  203. aResponse.SendResponse;
  204. end;
  205. else
  206. aResponse.Code:=400;
  207. aResponse.CodeText:='Wrong value for t';
  208. aResponse.SendResponse;
  209. end;
  210. S:=aRequest.QueryFields.Values['s'];
  211. Result:=(S='') or TryStrToBool(S,B);
  212. if not Result then
  213. begin
  214. aResponse.Code:=400;
  215. aResponse.CodeText:='Wrong value for s';
  216. aResponse.SendResponse;
  217. end;
  218. if not B then
  219. begin
  220. S:=aRequest.QueryFields.Values['m'];
  221. Result:=(S='') or TryStrToBool(S,B);
  222. if not Result then
  223. begin
  224. aResponse.Code:=400;
  225. aResponse.CodeText:='Wrong value for m';
  226. aResponse.SendResponse;
  227. end;
  228. end;
  229. end;
  230. Procedure THTTPSearcher.SetupMetadata;
  231. begin
  232. FMetadata:=TJSONObject.Create([
  233. 'root', 'data',
  234. 'idField','id',
  235. 'fields',TJSONArray.Create([
  236. TJSONObject.Create(['name','id','type','int']),
  237. TJSONObject.Create(['name','rank','type','int']),
  238. TJSONObject.Create(['name','url','type','string','maxlen',100]),
  239. TJSONObject.Create(['name','context','type','string','maxlen',MaxContextLen]),
  240. TJSONObject.Create(['name','date','type','date'])
  241. ])
  242. ]);
  243. FWordsMetadata:=TJSONObject.Create([
  244. 'root', 'data',
  245. 'idField','id',
  246. 'fields',TJSONArray.Create([
  247. TJSONObject.Create(['name','id','type','int']),
  248. TJSONObject.Create(['name','word','type','string','maxlen',100])
  249. ])
  250. ]);
  251. end;
  252. Function THTTPSearcher.InitSearch(aResponse : TResponse): Boolean;
  253. Const
  254. BaseName ='docsearch.ini';
  255. Function TestCfg(aDir : string) : String;
  256. begin
  257. Result:=aDir+BaseName;
  258. if not FileExists(Result) then
  259. Result:='';
  260. end;
  261. Var
  262. CFN : String;
  263. aIni: TMemIniFile;
  264. begin
  265. Result:=False;
  266. if FDB<>Nil then
  267. exit(True);
  268. try
  269. CFN:=TestCfg(GetAppConfigDir(true));
  270. if (CFN='') then
  271. CFN:=TestCfg(GetAppConfigDir(False));
  272. if (CFN='') then
  273. CFN:=TestCfg('config/');
  274. if (CFN='') then
  275. CFN:=TestCfg(ExtractFilePath(ParamStr(0)));
  276. if (CFN='') then
  277. CFN:=TestCfg('');
  278. if (CFN='') then
  279. Raise Exception.Create('No config file found');
  280. aIni:=TMemIniFile.Create(CFN);
  281. try
  282. FDB:=SetupDB(aIni);
  283. FFormattedJSON:=aIni.ReadBool('search','formatjson',False);
  284. FDefaultMinRank:=aIni.ReadInteger('search','minrank',1);
  285. FDefaultMetadata:=aIni.ReadBool('search','metadata',true);
  286. FAllowCors:=aIni.ReadBool('search','allowcors',true);
  287. finally
  288. aIni.Free;
  289. end;
  290. SetupMetadata;
  291. FSearch:=TFPSearch.Create(Self);
  292. FSearch.Database:=FDB;
  293. Result:=True;
  294. except
  295. On E : Exception do
  296. begin
  297. aResponse.Code:=500;
  298. aResponse.CodeText:='Could not set up search: '+E.Message;
  299. aResponse.SendResponse;
  300. end;
  301. end;
  302. end;
  303. Procedure THTTPSearcher.ConfigSearch(aRequest : TRequest; aResponse : TResponse);
  304. Var
  305. S : string;
  306. O : TSearchOptions;
  307. B : Boolean;
  308. begin
  309. FMinRank:=StrToIntDef(aRequest.QueryFields.Values['r'],0);
  310. if FMinRank=0 then
  311. FMinRank:=FDefaultMinRank;
  312. S:=aRequest.QueryFields.Values['m'];
  313. if (S='') or Not TryStrToBool(S,FIncludeMetaData) then
  314. FIncludeMetaData:=FDefaultMetaData;
  315. FSearch.SetSearchWord(aRequest.QueryFields.Values['q']);
  316. O:=[];
  317. S:=aRequest.QueryFields.Values['c'];
  318. if (S<>'') and TryStrToBool(S,B) and B then
  319. Include(O,soContains);
  320. FSearch.Options:=O;
  321. end;
  322. procedure THTTPSearcher.ConfigWordList(aRequest: TRequest; out aContaining: UTF8string; out Partial: TAvailableMatch; out aSimple: Boolean);
  323. Var
  324. m,S : String;
  325. begin
  326. aContaining:=aRequest.QueryFields.Values['q'];
  327. M:=aRequest.QueryFields.Values['t'];
  328. case lowercase(M) of
  329. 'all' : Partial:=amAll;
  330. 'contains' : Partial:=amContains;
  331. 'exact' : Partial:=amExact;
  332. 'startswith' : Partial:=amStartsWith;
  333. else
  334. Partial:=FDefaultAvailable;
  335. if (Partial<>amAll) and (aContaining='') then
  336. Partial:=amAll;
  337. end;
  338. S:=aRequest.QueryFields.Values['s'];
  339. if (S='') then
  340. aSimple:=False
  341. else
  342. aSimple:=StrToBool(S);
  343. if ASimple then
  344. FIncludeMetadata:=False
  345. else
  346. begin
  347. FIncludeMetaData:=FDefaultMetaData;
  348. S:=aRequest.QueryFields.Values['m'];
  349. if (S<>'') then
  350. TryStrToBool(S,FIncludeMetaData);
  351. end
  352. end;
  353. Function THTTPSearcher.SearchDataToJSON(aID : Integer;const aRes : TSearchWordData) : TJSONObject;
  354. begin
  355. Result:=TJSONObject.Create([
  356. 'id',aID,
  357. 'rank',aRes.Rank,
  358. 'url',aRes.URL,
  359. 'context',ares.Context,
  360. 'date',FormatDateTime('yyyy"-"mm"-"dd"T"hh":"nn":"ss',aRes.FileDate)
  361. ]);
  362. end;
  363. procedure THTTPSearcher.HTMLSearch(aRequest: TRequest; aResponse: TResponse);
  364. Var
  365. I : Integer;
  366. J : TJSONObject;
  367. A : TJSONArray;
  368. begin
  369. aResponse.ContentType:='application/json';
  370. if AllowCORS then
  371. AResponse.SetCustomHeader('Access-Control-Allow-Origin','*');
  372. if not CheckParams(aRequest,aResponse) then
  373. exit;
  374. if not InitSearch(aResponse) then
  375. exit;
  376. ConfigSearch(aRequest,aResponse);
  377. FSearch.Execute;
  378. A:=nil;
  379. J:=TJSONObject.Create;
  380. try
  381. if FIncludeMetadata then
  382. J.Add('metaData',FMetadata.Clone);
  383. A:=TJSONArray.Create;
  384. For I:=0 to Search.RankedCount-1 do
  385. begin
  386. if Search.RankedResults[I].Rank>=MinRank then
  387. A.Add(SearchDataToJSON(I+1,Search.RankedResults[I]));
  388. end;
  389. J.Add('data',A);
  390. SendJSON(J,aResponse);
  391. finally
  392. J.Free;
  393. end;
  394. end;
  395. procedure THTTPSearcher.SendJSON(J : TJSONObject; aResponse: TResponse);
  396. begin
  397. if FormattedJSON then
  398. aResponse.Content:=J.FormatJSON()
  399. else
  400. aResponse.Content:=J.AsJSON;
  401. aResponse.ContentLength:=Length(aResponse.Content);
  402. aResponse.SendContent;
  403. end;
  404. procedure THTTPSearcher.WordList(aRequest: TRequest; aResponse: TResponse);
  405. Var
  406. I : Integer;
  407. J : TJSONObject;
  408. A : TJSONArray;
  409. w,aContaining : UTF8String;
  410. aPartial : TAvailableMatch;
  411. aSimple : Boolean;
  412. aList : TUTF8StringArray;
  413. begin
  414. aResponse.ContentType:='application/json';
  415. if AllowCORS then
  416. AResponse.SetCustomHeader('Access-Control-Allow-Origin','*');
  417. if not CheckSearchParams(aRequest,aResponse) then
  418. exit;
  419. if not InitSearch(aResponse) then
  420. exit;
  421. ConfigWordList(aRequest,aContaining,aPartial,aSimple);
  422. FSearch.GetAvailableWords(aList,aContaining,aPartial);
  423. J:=TJSONObject.Create;
  424. try
  425. if FIncludeMetadata then
  426. J.Add('metaData',FWordsMetadata.Clone);
  427. A:=TJSONArray.Create;
  428. if aSimple then
  429. For W in aList do
  430. A.Add(W)
  431. else
  432. begin
  433. For I:=0 to Length(aList)-1 do
  434. A.Add(TJSONObject.Create(['id',I+1,'word',aList[i]]));
  435. end;
  436. J.Add('data',A);
  437. SendJSON(J,aResponse);
  438. finally
  439. J.Free;
  440. end;
  441. end;
  442. end.