unit uDataMod;

interface

uses
  System.SysUtils, System.Classes, FireDAC.Stan.Intf, FireDAC.Stan.Option,
  FireDAC.Stan.Error, FireDAC.UI.Intf, FireDAC.Phys.Intf, FireDAC.Stan.Def,
  FireDAC.Stan.Pool, FireDAC.Stan.Async, FireDAC.Phys, FireDAC.VCLUI.Wait,
  Data.DB, FireDAC.Comp.Client, FireDAC.Comp.UI, FireDAC.Phys.MSSQLDef,
  FireDAC.Phys.ODBCBase, FireDAC.Phys.MSSQL, FireDAC.Stan.Param, FireDAC.DatS,
  FireDAC.DApt.Intf, FireDAC.DApt, FireDAC.Comp.DataSet, FireDAC.Stan.Consts,
  Winapi.ActiveX, System.RegularExpressions,
  MVCFramework.Logger,
  Quick.Logger, Quick.Threads, Quick.Logger.Provider.Files;


const
{$I globalConsts.inc}

type
  TdatMod = class(TDataModule)
    sqlConn: TFDConnection;
    sqlWaitCur: TFDGUIxWaitCursor;
    sqlMSSQLDrv: TFDPhysMSSQLDriverLink;
    sqlQry1: TFDQuery;
    sqlQry2: TFDQuery;
    sqlQry3: TFDQuery;
    sqlQry10: TFDQuery;
    sqlQry11: TFDQuery;
    qryZpracPrijataData: TFDQuery;
    procedure DataModuleDestroy(Sender: TObject);
   private
    constructor Create (AOwner: TComponent); override;
    function InterniConnectDB (odbc:Boolean=false; pool:Boolean=true): string;
   public
    dbName, dbServer, dbUser, dbPwd, cfgComp: string;
    dbPort: integer;
    dbEncConn, dbConnOleDB, chL: boolean;
    sqlConnParams: TStringList;
    Logger: TLogFileProvider;
    function ConnectServer: boolean;
    function ErrorStatusJsonText (const errText: string): string;
    function SQLTableExists (const tabName: string): Boolean;
    function SQLRecordExists (sqlText: string): Boolean;
    function SQLColumnExists (const tabName: string; const colName: string): Boolean;
    function SQLIDsToString (const tabName: string=''; where: string=''): string;
    function SQLIsSafe (const S: string): boolean;
    function SQLGetString (sqlCmd: string=''; sqlConnLoc: TFDConnection=nil): string;

    function StreamToString (const M: TStream): string;
    function MemoryStreamToString (const M: TMemoryStream): string;
    function MemoryStreamToAnsiString (const M: TMemoryStream): AnsiString;

    procedure CreateLogger (fileName: string='');
    procedure LogInfo (eTyp: Quick.Logger.TEventType; const msg: string);
  end;

var
  datMod: TdatMod;
  ConnDef: IFDStanConnectionDef;
  ConnInit: Boolean = false;
  ConnInitLock: TObject;


implementation
{%CLASSGROUP 'Vcl.Controls.TControl'}
{$R *.dfm}
uses System.StrUtils, Data.Win.ADODB,
     System.JSON.Builders, System.JSON.Writers, System.JSON.Types, JsonDataObjects,
     uWinService;




 constructor TdatMod.Create (AOwner: TComponent);
  begin
   inherited Create (AOwner);
   CreateLogger;
  end;




 procedure TdatMod.CreateLogger (fileName: string='');
  begin
   if (fileName='') then
    fileName:= 'hdcDZApi.log';
   Logger:= TLogFileProvider.Create;
   Logger.AutoFlush:= true;
   Logger.FileName:= ExtractFilePath(ParamStr(0)) + fileName;
   Logger.Init;
  end;




 procedure TdatMod.DataModuleDestroy (Sender: TObject);
  begin
   if (sqlConnParams<>nil) then
    sqlConnParams.Free;
  end;



 function TdatMod.StreamToString (const M: TStream): string;
  var s: TStringStream;
  begin
   result:= '';
   if (M<>nil) then
    begin
     s:= TStringStream.Create;
     try
       s.CopyFrom (M, 0);
       result:= s.DataString;
      finally
       s.Free;
     end;
    end;
  end;


 function TdatMod.MemoryStreamToString (const M: TMemoryStream): string;
  begin
   SetString(Result, PWideChar(M.Memory), M.Size div SizeOf(Char));
  end;



 function TdatMod.MemoryStreamToAnsiString (const M: TMemoryStream): AnsiString;
  begin
   SetString (Result, PAnsiChar(M.Memory), M.Size);
  end;



 procedure TdatMod.LogInfo (eTyp: Quick.Logger.TEventType; const msg: string);
  begin
   logItem.EventType:= eTyp;
   logItem.EventDate:= now;
   logItem.Msg:= msg;
   Logger.WriteLog(logItem);
  end;



 function TdatMod.InterniConnectDB (odbc: Boolean = False; pool: Boolean = True): string;
  var drvList: TStringList;
      errMsg, connStr, drvName, drvName2: string;
      mamOdbc, mamOleDb: Boolean;
      idx: integer;
      maxNum: byte;
      oPars: TStrings; // TFDPhysMSSQLConnectionDefParams;
  begin
   result:= '';
   mamOdbc:= false;
   mamOleDb:= false;
   drvName:= 'MSSQL';
   drvName2:= 'SQL Server';

   dbConnOleDB:= false;


   if not(sqlConn.Connected) and (dbServer<>'') and (dbName<>'') then
    begin
     try
       oPars:= TStringList.Create;
       try
//         oPars.Add(S_FD_ConnParam_Common_DriverID + '=MSSQL');  // je definovan v AddConnectionDef
         oPars.Add(S_FD_ConnParam_Common_Server + '=' + dbServer);
         oPars.Add(S_FD_ConnParam_Common_Port + '=' + dbPort.ToString);
         oPars.Add(S_FD_ConnParam_Common_Database + '=' + dbName);
         oPars.Add(S_FD_ConnParam_Common_UserName + '=' + dbUser);
         oPars.Add(S_FD_ConnParam_Common_Password + '=' + dbPwd);
         oPars.Add(S_FD_ConnParam_Common_OSAuthent + '=No');
         oPars.Add(S_FD_ConnParam_Common_MetaDefSchema + '=dbo');
         oPars.Add(S_FD_ConnParam_Common_LoginTimeout + '=15');
         oPars.Add(S_FD_ConnParam_MSSQL_MARS + '=Yes');
         oPars.Add(S_FD_ConnParam_Common_ExtendedMetadata + '=False'); // True jen pri pouziti AutoGenerate SQL / CachedUpdates / UpdateObject
         oPars.Add(S_FD_ConnParam_Common_ApplicationName + '=hdcdzAPIsvc');
         oPars.Add(S_FD_ConnParam_Common_Pooled + '=True');
         oPars.Add(S_FD_ConnParam_Common_Pool_MaximumItems + '=300');
         oPars.Add(S_FD_ConnParam_Common_Pool_ExpireTimeout + '=30000');  // FireDAC v intervalu 30 sekund testuje platnost spojeni, jestli to SQL server neukoncil
         oPars.Add(S_FD_ConnParam_Common_Pool_CleanupTimeout + '=90000');  // FireDAC v intervalu 90 sekund cisti nepouzivana spojeni
         oPars.Add(S_FD_ConnParam_ODBC_ODBCAdvanced + '=TrustServerCertificate=yes');
//         oPars.Add('CommandTimeout=120');
         if (dbEncConn) then
          oPars.Add(S_FD_ConnParam_MSSQL_Encrypt + '=Yes');

         if not(ConnInit) then
          begin
           TMonitor.Enter(ConnInitLock);
           try
             if not(ConnInit) then
              begin
               if (FDManager.ConnectionDefs.FindConnectionDef(sqlPoolName) = nil) then
                FDManager.AddConnectionDef(sqlPoolName, 'MSSQL', oPars);
               if (FDManager.State = dmsInactive) then
                FDManager.Open;
               ConnInit := True;
              end;
            finally
             TMonitor.Exit (ConnInitLock);
           end;
          end;

//       sqlConn.Params.SetStrings(oPars);
         sqlConn.ConnectionDefName:= sqlPoolName;
         sqlQry1.Connection:= sqlConn;
         sqlQry2.Connection:= sqlConn;
         sqlQry3.Connection:= sqlConn;
         sqlQry10.Connection:= sqlConn;
         sqlQry11.Connection:= sqlConn;

//       if (dbConnOleDB) then
//         sqlMSSQLDrv.ODBCDriver:= '';

         connStr:= '';
         if (dbEncConn) then
          begin
           if (dbConnOleDB) then
             connStr:= ';Encrypt=True;TrustServerCertificate=True;UseEncryptionForData=True'
            else
             connStr:= ';Encrypt=yes;TrustServerCertificate=yes;Encrypt=yes';
//         sqlConn.Params.Add(S_FD_ConnParam_ODBC_ODBCAdvanced + '=' + connStr);
          end;
         connStr:= FDManager.ConnectionDefs.ConnectionDefByName(sqlPoolName).Params.Text + connStr;

         sqlConn.Open;
         sqlConn.ExecSQL('SET LOCK_TIMEOUT 60000');  // čekej max 60 sekund na lock, ne nekonecne jako je default (-1)
        except on E:Exception do
         begin
          result:= E.Message;
{$IFDEF DEBUG}
          Writeln('Chyba spojeni - ' + E.ClassName, ': ', result);
{$ENDIF}
         end;
       end; // try
      finally
//       sqlConnParams.Free;
       oPars.Free;
     end; // try
    end;
  end;




 function TdatMod.ConnectServer: Boolean;
  var drvList: TStringList;
      errMsg, connStr, drvName, drvName2: string;
      mamOdbc, mamOleDb: Boolean;
      idx: integer;
      maxNum: byte;
  begin
   try
     errMsg:= InterniConnectDB (false);
     if (errMsg.Contains('ogin failed for us')) or (errMsg.Contains('ogin failed')) then
      errMsg:= InterniConnectDB (true);

     if (errMsg<>'') then
      LogInfo (etWarning, 'Chyba spojeni: ' + errMsg);
    except on E:Exception do
     errMsg:= E.Message;
   end;

   result:= (errMsg='');
  end;





 function TdatMod.ErrorStatusJsonText (const errText: string): string;
  var b: TJSONObjectBuilder;
      w: TJsonTextWriter;
      sB: TStringBuilder;
      sW: TStringWriter;
      p: TJSONCollectionBuilder.TPairs;
      o: TJSONObject;
  begin
   o:= TJSONObject.Create;
   sB:= TStringBuilder.Create;
   sW:= TStringWriter.Create(sB);
   w:= TJsonTextWriter.Create(sW);
   w.Formatting:= TJsonFormatting.Indented;
   b:= TJSONObjectBuilder.Create(w);
//    b.BeginObject.BeginArray('status').BeginObject.Add('status', status).EndObject.EndArray.EndObject;
   p:= b.BeginObject.Add('status', errText);
   p.EndObject;

   o.FromJSON(sB.ToString);
   result:= o.ToString;
  end;




 function TdatMod.SQLTableExists (const tabName: string): Boolean;
  var lSQL, s, t: string;
      lQry: TFDQuery;
      sqlConnX: TFDConnection;
      c: boolean;
  begin
   result:= false;

   sqlConnX:= TFDConnection.Create(nil);
   sqlConnX.ConnectionDefName:= sqlPoolName;
//   sqlConnX.DriverName:= 'MSSQL';
   try
     sqlConnX.Open;
    finally
   end;
   c:= sqlConnX.Connected;

   if (c) then
    begin
     s:= '';
     t:= tabName.Replace('[', '', [rfReplaceAll]).Replace(']', '', [rfReplaceAll]);
     if (t.ToLower.Contains('dbo.')) then
      begin
       s:= 'dbo';
       t:= t.Replace('dbo.', '', [rfIgnoreCase]);
      end;
     lSQL:= 'SELECT 1 AS A FROM INFORMATION_SCHEMA.COLUMNS c WHERE c.TABLE_NAME=N' + t.QuotedString + IfThen(s<>'', ' AND c.TABLE_SCHEMA=N' + s.QuotedString, '');
     lQry:= TFDQuery.Create(nil);
     try
       lQry.Connection:= sqlConnX;
       try
         lQry.Open(lSQL);
         result:= (lQry.RecordCount>0);
//         Log.Info('SQLTableExists OK', 'uDataMod');
        except on E:Exception do
         begin
          Log.Warn('Chyba SQLTableExists: ' + E.Message + CRLF + lSQL, 'uDataMod');
          result:= false;
         end;
       end;
      finally
       lQry.Free;
     end;
    end;

   sqlConnX.Close;
   sqlConnX.Free;

  end;





 function TdatMod.SQLRecordExists (sqlText: string): boolean;
  var lSQL, s, t: string;
      lQry: TFDQuery;
      sqlConnX: TFDConnection;
      c: boolean;
  begin
   result:= false;

   sqlConnX:= TFDConnection.Create(nil);
   sqlConnX.Params:= sqlConn.Params;
   try
     sqlConnX.Open;
    finally
   end;
   c:= sqlConnX.Connected;

   sqlText:= sqlText.Replace(';', '').Replace('--', '');

   if (c) and (sqlText<>'') then
    begin
     lQry:= TFDQuery.Create(nil);
     if (sqlConnX<>nil) then
       lQry.Connection:= sqlConnX
      else
       lQry.Connection:= sqlConn;
     try
       try
         lQry.Open(sqlText);
         result:= (lQry.RecordCount>0);
        except on E:Exception do
         begin
          Log.Warn('Chyba SQLRecordExists: ' + E.Message + CRLF + lSQL, 'uDataMod');
          result:= false;
         end;
       end;
      finally
       lQry.Free;
     end;
    end;

   if (sqlConnX<>nil) then
    begin
     sqlConnX.Close;
     sqlConnX.Free;
    end;

  end;




 function TdatMod.SQLIsSafe (const S: string): boolean;
  var UpperS: string;
  begin
   result:= TRegEx.IsMatch (S, '^[A-Za-z0-9_ \(\)=<>!\.,]+$', [roIgnoreCase]);
   if not result then
    Exit;

   UpperS := UpperCase(S);
// zakázaná klíčová slova
   if TRegEx.IsMatch(UpperS, '\b(SELECT|INSERT|UPDATE|DELETE|DROP|EXEC|UNION)\b') then
    Exit (False);

   result := True;
  end;




 function TdatMod.SQLIDsToString (const tabName: string = ''; where: string = ''): string;
  var lSQL, s, t: string;
      c: boolean;
      lQry: TFDQuery;
      sqlConnX: TFDConnection;
  begin
   result := '';

   try
    if (self.sqlConn.Connected) then
     begin
      s := '';
      t := tabName;
      if not(t.Contains('dbo.')) and not(t.Contains('dbo].')) then
       t:= '[' + t;
      if not(t.EndsWith(']')) then
       t:= t + ']';
      if not(t.Contains('dbo.')) and not(t.Contains('dbo].')) then
       t:= '[dbo].' + t;


      sqlConnX := TFDConnection.Create(nil);
      sqlConnX.ConnectionDefName := sqlPoolName;
      try
        sqlConnX.Open;
       finally
      end;
      c:= sqlConnX.Connected;

      if (c) then
       begin
        lQry:= TFDQuery.Create(nil);
        try
          result := '';
          lQry.Connection := sqlConnX;
          where:= where.Replace(';', '').Replace('--', '').Trim; // sanitace podminky
          lSQL:= 'SELECT STRING_AGG(ID,'','') AS IDs FROM ' + t + IfThen(where<>'', ' WHERE ' + where, '');

          if not(SQLIsSafe(where)) then
           lSQL:= '';

          if (lSQL<>'') then
           begin
            try
              lQry.Open (lSQL);
              result := lQry.FieldByName('IDs').AsString;
             except on E:Exception do
              result := '';
            end;
           end;
         finally
          lQry.Free;
        end;

        sqlConnX.Close;
       end;

      sqlConnX.Free;
     end;
    except on E:Exception do
     LogInfo (Quick.Logger.etInfo, 'In SQLIDsToString error: ' + E.Message);
   end;

  end;




 function TdatMod.SQLColumnExists (const tabName: string; const colName: string): Boolean;
  var lSQL, s, t: string;
      lQry: TFDQuery;
      c: boolean;
      sqlConnX: TFDConnection;
  begin
   result:= false;

   sqlConnX:= TFDConnection.Create(nil);
   sqlConnX.ConnectionDefName:= sqlPoolName;
   c:= sqlConnX.Connected;

   if (c) then
    begin
     s:= '';
     t:= tabName.Replace('[', '', [rfReplaceAll]).Replace(']', '', [rfReplaceAll]);
     if (t.ToLower.Contains('dbo.')) then
      begin
       s:= 'dbo';
       t:= t.Replace('dbo.', '', [rfIgnoreCase]);
      end;

     lQry:= TFDQuery.Create(nil);
     try
       lQry.Connection:= sqlConnX;
       lSQL:= 'SELECT 1 AS A FROM INFORMATION_SCHEMA.COLUMNS c WHERE c.TABLE_NAME=N' + t.QuotedString + IfThen(s<>'', ' AND c.TABLE_SCHEMA=N' + s.QuotedString, '');
       lSQL:= lSQL + ' AND c.COLUMN_NAME=N' + colName.QuotedString;
       try
         lQry.Open(lSQL);
         result:= (lQry.RecordCount>0);
        except on E:Exception do
         begin
          Log.Warn('Chyba SQLColumnExists: ' + E.Message + CRLF + lSQL, 'uDataMod');
          result:= false;
         end;
       end;
      finally
       lQry.Free;
     end;
     sqlConnX.Close;
    end;

   sqlConnX.Free;
  end;




 function TdatMod.SQLGetString (sqlCmd: string=''; sqlConnLoc: TFDConnection=nil): string;
  var lSQL, s, t: string;
      lQry: TFDQuery;
      c: boolean;
      sqlConnX: TFDConnection;
  begin
   result:= '';

   if (sqlConnLoc<>nil) then
     sqlConnX:= sqlConnLoc
    else
     begin
      sqlConnX:= TFDConnection.Create(nil);
      sqlConnX.ConnectionDefName:= sqlPoolName;
     end;


   lQry:= TFDQuery.Create(nil);
   try
     lQry.Connection:= sqlConnX;
     try
       lQry.Open(sqlCmd);
       result:= lQry.Fields.Fields[0].AsString;
      except on E:Exception do
       begin
        Log.Warn('Chyba SQLGetString: ' + E.Message + CRLF + lSQL, 'uDataMod');
       end;
     end;
    finally
     lQry.Free;
   end;

   if (sqlConnLoc=nil) then
    begin
     sqlConnX.Close;
     FreeAndNil (sqlConnX);
    end;
  end;



 initialization
  ConnInitLock := TObject.Create;
//  CoInitialize(nil);


 finalization
  ConnInitLock.Free;
//  CoUninitialize;

end.
