module Database.Design.Ampersand.Prototype.PHP ( executePHPStr, executePHP, showPHP, sqlServerConnectPHP, createTempDbPHP, setSqlModePHP , evaluateExpSQL, performQuery , createTablesPHP, populateTablesPHP, populateTablesWithPopsPHP, plug2TableSpec , dropplug, historyTableSpec, sessionTableSpec, signalTableSpec, TableSpec, getTableName) where import Prelude hiding (exp) import Control.Exception import Control.Monad import Data.List import System.Process import System.IO hiding (hPutStr,hGetContents) import System.Directory import Database.Design.Ampersand hiding (putStr, origin) import Database.Design.Ampersand.Prototype.ProtoUtil import Database.Design.Ampersand.FSpec.SQL fatal :: Int -> String -> a fatal = fatalMsg "PHP" createTablesPHP :: FSpec -> [String] createTablesPHP fSpec = [ "/*** Create new SQL tables ***/" , "" ] ++ concatMap createTablePHP [sessionTableSpec, historyTableSpec] ++ [ "$time = explode(' ', microTime()); // copied from DatabaseUtils setTimestamp" , "$microseconds = substr($time[0], 2,6);" , "$seconds =$time[1].$microseconds;" , "date_default_timezone_set(\"Europe/Amsterdam\");" -- to prevent a php warning TODO: check if this is ok when Ampersand is used in different timezones , "$date = date(\"j-M-Y, H:i:s.\").$microseconds;" , "mysqli_query($DB_link, \"INSERT INTO `__History__` (`Seconds`,`Date`) VALUES ('$seconds','$date')\");" , "if($err=mysqli_error($DB_link)) {" , " $error=true; echo $err.'
';" , "}" , "" ] ++setSqlModePHP++ createTablePHP signalTableSpec ++ [ "" , "//// Number of plugs: " ++ show (length (plugInfos fSpec)) ] -- Create all plugs ++ concatMap (createTablePHP . plug2TableSpec) [p | InternalPlug p <- plugInfos fSpec] -- (headerCmmnt,tableName,crflds,engineOpts) type TableSpec = (String,String,[String],String) getTableName :: TableSpec -> String getTableName (_,nm,_,_) = nm createTablePHP :: TableSpec -> [String] createTablePHP (headerCmmnt,tableName,crflds,engineOpts) = [ headerCmmnt -- Drop table if it already exists , "if($columns = mysqli_query($DB_link, "++showPhpStr ("SHOW COLUMNS FROM `"++tableName++"`")++")){" , " mysqli_query($DB_link, "++showPhpStr ("DROP TABLE `"++tableName++"`")++");" , "}" ] ++ [ "mysqli_query($DB_link,\"CREATE TABLE `"++tableName++"`"] ++ [ replicate 23 ' ' ++ [pref] ++ " " ++ fld | (pref, fld) <- zip ('(' : repeat ',') crflds ] ++ [ replicate 23 ' ' ++ ") ENGINE=" ++engineOpts ++ "\");"]++ [ "if($err=mysqli_error($DB_link)) {" , " $error=true; echo $err.'
';" , "}" , "" ]++setSqlModePHP plug2TableSpec :: PlugSQL -> TableSpec plug2TableSpec plug = ( unlines $ commentBlock (["Plug "++name plug,"","fields:"]++map (\x->showADL (fldexpr x)++" "++show (multiplicities $ fldexpr x)) (plugFields plug)) , name plug , [ quote (fldname f)++" " ++ showSQL (fldtype f) ++ (if fldauto f then " AUTO_INCREMENT" else " DEFAULT NULL") | f <- plugFields plug ]++ case (plug, (head.plugFields) plug) of (BinSQL{}, _) -> [] (_, primFld) -> case flduse primFld of TableKey isPrim _ -> [ (if isPrim then "PRIMARY " else "") ++ "KEY (`"++fldname primFld++"`)" ] ForeignKey c -> fatal 195 ("ForeignKey "++name c++"not expected here!") PlainAttr -> [] , "InnoDB DEFAULT CHARACTER SET UTF8") signalTableSpec :: TableSpec signalTableSpec = ( "// Signal table" , "__all_signals__" , [ "`conjId` VARCHAR(255) NOT NULL" , "`src` VARCHAR(255) NOT NULL" , "`tgt` VARCHAR(255) NOT NULL" ] , "InnoDB DEFAULT CHARACTER SET UTF8" ) sessionTableSpec :: TableSpec sessionTableSpec = ( "// Session timeout table" , "__SessionTimeout__" , [ "`SESSION` VARCHAR(255) UNIQUE NOT NULL" , "`lastAccess` BIGINT NOT NULL" ] , "InnoDB DEFAULT CHARACTER SET UTF8" ) historyTableSpec :: TableSpec historyTableSpec = ( "// Timestamp table" , "__History__" , [ "`Seconds` VARCHAR(255) DEFAULT NULL" , "`Date` VARCHAR(255) DEFAULT NULL" ] , "InnoDB DEFAULT CHARACTER SET UTF8" ) populateTablesPHP :: FSpec -> [String] populateTablesPHP fSpec = fillSignalTable (initialConjunctSignals fSpec) ++ populateTablesWithPopsPHP fSpec (initialPops fSpec) where fillSignalTable [] = [] fillSignalTable conjSignals = [ "mysqli_query($DB_link, "++showPhpStr ("INSERT INTO "++ quote (getTableName signalTableSpec) ++" (`conjId`, `src`, `tgt`)" ++phpIndent 24++"VALUES " ++ intercalate (phpIndent 29++", ") [ "(" ++sqlConjId++", "++sqlAtomQuote (srcPaire p)++", "++sqlAtomQuote (trgPaire p)++")" | (conj, viols) <- conjSignals , let sqlConjId = "'" ++ rc_id conj ++ "'" -- conjunct id's do not need escaping , p <- viols ])++"\n"++ " );" , "if($err=mysqli_error($DB_link)) { $error=true; echo $err.'
'; }" ] populateTablesWithPopsPHP :: FSpec -> [Population] -> [String] populateTablesWithPopsPHP fSpec pops = concatMap populatePlugPHP [p | InternalPlug p <- plugInfos fSpec] where populatePlugPHP plug = case tblcontents (vgens fSpec) pops plug of [] -> [] tblRecords -> ( "mysqli_query($DB_link, "++showPhpStr ("INSERT INTO "++quote (name plug) ++" ("++intercalate "," [quote (fldname f) |f<-plugFields plug]++")" ++phpIndent 17++"VALUES " ++ intercalate (phpIndent 22++", ") [ "(" ++valuechain md++ ")" | md<-tblRecords] ++phpIndent 16 ) ++");" ): ["if($err=mysqli_error($DB_link)) { $error=true; echo $err.'
'; }"] where valuechain record = intercalate ", " [case fld of Nothing -> "NULL" ; Just str -> sqlAtomQuote str | fld<-record] dropplug :: PlugSQL -> String dropplug plug = "DROP TABLE "++quote (name plug)++"" sqlServerConnectPHP :: FSpec -> [String] sqlServerConnectPHP fSpec = [ "// Try to connect to the database" , "global $DB_host,$DB_user,$DB_pass;" , "$DB_host='"++addSlashes (sqlHost (getOpts fSpec))++"';" , "$DB_user='"++addSlashes (sqlLogin (getOpts fSpec))++"';" , "$DB_pass='"++addSlashes (sqlPwd (getOpts fSpec))++"';" , "$DB_link = mysqli_connect($DB_host,$DB_user,$DB_pass);" , "// Check connection" , "if (mysqli_connect_errno()) {" , " die(\"Failed to connect to MySQL: \" . mysqli_connect_error());" , "}" , "" ]++setSqlModePHP createTempDbPHP :: String -> [String] createTempDbPHP dbNm = [ "$DB_name='"++addSlashes dbNm++"';" , "// Drop the database if it exists" , "$sql=\"DROP DATABASE $DB_name\";" , "mysqli_query($DB_link,$sql);" , "// Don't bother about the error if the database didn't exist..." , "" , "// Create the database" , "$sql=\"CREATE DATABASE $DB_name DEFAULT CHARACTER SET UTF8\";" , "if (!mysqli_query($DB_link,$sql)) {" , " die(\"Error creating the database: \" . mysqli_error($DB_link));" , " }" , "" , "// Connect to the freshly created database" , "$DB_link = mysqli_connect($DB_host,$DB_user,$DB_pass,$DB_name);" , "// Check connection" , "if (mysqli_connect_errno()) {" , " die(\"Failed to connect to the database: \" . mysqli_connect_error());" , " }" , "" ]++setSqlModePHP -- evaluate normalized exp in SQL evaluateExpSQL :: FSpec -> String -> Expression -> IO [(String,String)] evaluateExpSQL fSpec dbNm exp = fmap sort (performQuery (getOpts fSpec) dbNm violationsQuery) where violationsExpr = conjNF (getOpts fSpec) exp violationsQuery = prettySQLQuery fSpec 26 violationsExpr performQuery :: Options -> String -> String -> IO [(String,String)] performQuery opts dbNm queryStr = do { queryResult <- (executePHPStr . showPHP) php ; if "Error" `isPrefixOf` queryResult -- not the most elegant way, but safe since a correct result will always be a list then do verboseLn opts{verboseP=True} ("\n******Problematic query:\n"++queryStr++"\n******") fatal 141 $ "PHP/SQL problem: "++queryResult else case reads queryResult of [(pairs,"")] -> return pairs _ -> fatal 143 $ "Parse error on php result: "++show queryResult } where php = [ "// Try to connect to the database" , "$DB_name='"++addSlashes dbNm++"';" , "global $DB_host,$DB_user,$DB_pass;" , "$DB_host='"++addSlashes (sqlHost opts)++"';" , "$DB_user='"++addSlashes (sqlLogin opts)++"';" , "$DB_pass='"++addSlashes (sqlPwd opts)++"';" , "$DB_link = mysqli_connect($DB_host,$DB_user,$DB_pass,$DB_name);" , "// Check connection" , "if (mysqli_connect_errno()) {" , " die(\"Error: Failed to connect to $DB_name: \" . mysqli_connect_error());" , " }" , "" ]++setSqlModePHP++ [ "$sql="++showPhpStr queryStr++";" , "$result=mysqli_query($DB_link,$sql);" , "if(!$result)" , " die('Error '.($ernr=mysqli_errno($DB_link)).': '.mysqli_error($DB_link).'(Sql: $sql)');" , "$rows=Array();" , " while ($row = mysqli_fetch_array($result)) {" , " $rows[]=$row;" , " unset($row);" , " }" , "echo '[';" , "for ($i = 0; $i < count($rows); $i++) {" , " if ($i==0) echo ''; else echo ',';" , " echo '(\"'.addslashes($rows[$i]['src']).'\", \"'.addslashes($rows[$i]['tgt']).'\")';" , "}" , "echo ']';" ] -- call the command-line php with phpStr as input executePHPStr :: String -> IO String executePHPStr phpStr = do { tempdir <- catch getTemporaryDirectory (\e -> do let err = show (e :: IOException) hPutStr stderr ("Warning: Couldn't find temp directory. Using current directory : " ++ err) return ".") ; (tempPhpFile, temph) <- openTempFile tempdir "phpInput" ; hPutStr temph phpStr ; hClose temph ; results <- executePHP Nothing tempPhpFile [] ; removeFile tempPhpFile ; return results } executePHP :: Maybe String -> String -> [String] -> IO String executePHP mWorkingDir phpPath phpArgs = do { let cp = CreateProcess { cmdspec = RawCommand "php" $ phpPath : phpArgs , cwd = mWorkingDir , env = Just [("TERM","dumb")] -- environment , std_in = Inherit , std_out = CreatePipe , std_err = CreatePipe , close_fds = False -- no need to close all other file descriptors , create_group = False , delegate_ctlc = False -- don't let php handle ctrl-c } ; (_, mStdOut, mStdErr, _) <- createProcess cp ; outputStr <- case (mStdOut, mStdErr) of (Nothing, _) -> fatal 44 "no output handle" (_, Nothing) -> fatal 45 "no error handle" (Just stdOutH, Just stdErrH) -> do { --putStrLn "done" ; errStr <- hGetContents stdErrH ; seq (length errStr) $ return () ; hClose stdErrH ; unless (null errStr) $ putStrLn $ "Error during PHP execution:\n" ++ errStr ; outputStr' <- hGetContents stdOutH --and fetch the results from the output pipe ; seq (length outputStr') $ return () ; hClose stdOutH ; return outputStr' } -- ; putStrLn $ "Results:\n" ++ outputStr ; return outputStr } showPHP :: [String] -> String showPHP phpLines = unlines $ [""] -- | php code snippet to set the sql_mode setSqlModePHP :: [String] setSqlModePHP = [ "$sql=\"SET SESSION sql_mode = 'ANSI,TRADITIONAL'\";" -- ANSI because of the syntax of the generated SQL -- TRADITIONAL because of some more safety , "if (!mysqli_query($DB_link,$sql)) {" , " die(\"Error setting sql_mode: \" . mysqli_error($DB_link));" , " }" , "" ]