Subversion Repositories oidplus

Rev

Rev 1364 | Rev 1370 | Go to most recent revision | Blame | Compare with Previous | Last modification | View Log | RSS feed

  1. <?php
  2.  
  3. /*
  4.  * OIDplus 2.0
  5.  * Copyright 2019 - 2023 Daniel Marschall, ViaThinkSoft
  6.  *
  7.  * Licensed under the Apache License, Version 2.0 (the "License");
  8.  * you may not use this file except in compliance with the License.
  9.  * You may obtain a copy of the License at
  10.  *
  11.  *     http://www.apache.org/licenses/LICENSE-2.0
  12.  *
  13.  * Unless required by applicable law or agreed to in writing, software
  14.  * distributed under the License is distributed on an "AS IS" BASIS,
  15.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  16.  * See the License for the specific language governing permissions and
  17.  * limitations under the License.
  18.  */
  19.  
  20. namespace ViaThinkSoft\OIDplus;
  21.  
  22. // phpcs:disable PSR1.Files.SideEffects
  23. \defined('INSIDE_OIDPLUS') or die;
  24. // phpcs:enable PSR1.Files.SideEffects
  25.  
  26. class OIDplusDatabaseConnectionADO extends OIDplusDatabaseConnection {
  27.         /**
  28.          * @var mixed|null
  29.          */
  30.         private $conn = null;
  31.  
  32.         /**
  33.          * @var string|null
  34.          */
  35.         private $last_error = null; // do the same like MySQL+PDO, just to be equal in the behavior
  36.  
  37.         /**
  38.          * @param string $sql
  39.          * @param array|null $prepared_args
  40.          * @return OIDplusQueryResultADO
  41.          * @throws OIDplusConfigInitializationException
  42.          * @throws OIDplusException
  43.          * @throws OIDplusSQLException
  44.          */
  45.         protected function doQueryPrepareEmulation(string $sql, array $prepared_args=null): OIDplusQueryResultADO {
  46.                 $sql = str_replace('?', chr(1), $sql);
  47.                 foreach ($prepared_args as $arg) {
  48.                         $needle = chr(1);
  49.                         if (is_bool($arg)) {
  50.                                 if ($this->slangDetectionDone) {
  51.                                         $replace = $this->getSlang()->getSQLBool($arg);
  52.                                 } else {
  53.                                         $replace = $arg ? '1' : '0';
  54.                                 }
  55.                         } else if (is_int($arg)) {
  56.                                 $replace = $arg;
  57.                         } else if (is_float($arg)) {
  58.                                 $replace = number_format($arg, 10, '.', '');
  59.                         } else if (is_null($arg)) {
  60.                                 $replace = 'NULL';
  61.                         } else {
  62.                                 // TODO: More types?
  63.                                 if ($this->slangDetectionDone) {
  64.                                         $replace = "N'".$this->getSlang()->escapeString($arg)."'";
  65.                                 } else {
  66.                                         $replace = "N'".str_replace("'", "''", $arg)."'";
  67.                                 }
  68.                         }
  69.                         $pos = strpos($sql, $needle);
  70.                         if ($pos !== false) {
  71.                                 $sql = substr_replace($sql, $replace, $pos, strlen($needle));
  72.                         }
  73.                 }
  74.                 $sql = str_replace(chr(1), '?', $sql);
  75.                 return $this->doQuery($sql);
  76.         }
  77.  
  78.         /**
  79.          * @var int
  80.          */
  81.         private $rowsAffected = 0;
  82.  
  83.         /**
  84.          * @return int
  85.          */
  86.         public function rowsAffected(): int {
  87.                 return $this->rowsAffected;
  88.         }
  89.  
  90.         /**
  91.          * @param string $sql
  92.          * @param array|null $prepared_args
  93.          * @return OIDplusQueryResultADO
  94.          * @throws OIDplusException
  95.          */
  96.         public function doQuery(string $sql, array $prepared_args=null): OIDplusQueryResult {
  97.                 $this->last_error = null;
  98.                 if (is_null($prepared_args)) {
  99.                         try {
  100.                                 $fetchableRowsExpected = $this->slangDetectionDone ? $this->getSlang()->fetchableRowsExpected($sql) : str_starts_with(trim(strtolower($sql)),'select');
  101.                                 if ($fetchableRowsExpected) {
  102.                                         $res = new \COM("ADODB.Recordset", NULL, 65001/*CP_UTF8*/);
  103.  
  104.                                         $res->Open($sql, $this->conn, 3/*adOpenStatic*/, 3/*adLockOptimistic*/);   /** @phpstan-ignore-line */
  105.  
  106.                                         $deb = new OIDplusQueryResultADO($res);
  107.  
  108.                                         // These two lines are important, otherwise INSERT queries won't have @@ROWCOUNT and stuff...
  109.                                         // It's probably this an MARS issue (multiple result sets open at the same time),
  110.                                         // especially because the __destruct() raises an Exception that the dataset is already closed...
  111.                                         $deb->prefetchAll();
  112.                                         $res->Close(); /** @phpstan-ignore-line */
  113.  
  114.                                         // Important: Do num_rows() after prefetchAll(), because
  115.                                         // at OLE DB provider for SQL Server, RecordCount is -1 for queries
  116.                                         // which don't have physical row tables, e.g. "select max(id) as maxid from ###log"
  117.                                         // If we have prefetched the table, then RecordCount won't be checked;
  118.                                         // instead, the prefetched array will be counted.
  119.                                         $this->rowsAffected = $deb->num_rows();
  120.  
  121.                                         return $deb;
  122.  
  123.                                 } else {
  124.                                         $this->conn->Execute($sql, $this->rowsAffected);
  125.  
  126.                                         // Alternatively:
  127.                                         //$cmd = new \COM("ADODB.Command", NULL, 65001/*CP_UTF8*/);
  128.                                         //$cmd->CommandText = $sql;
  129.                                         //$cmd->CommandType = 1/*adCmdText*/;
  130.                                         //$cmd->ActiveConnection = $this->conn;
  131.                                         //$cmd->Execute();
  132.  
  133.                                         return new OIDplusQueryResultADO(null);
  134.                                 }
  135.                         } catch (\Exception $e) {
  136.                                 $this->last_error = $e->getMessage();
  137.                                 throw new OIDplusSQLException($sql, $this->error());
  138.                         }
  139.  
  140.                 } else {
  141.                         return $this->doQueryPrepareEmulation($sql, $prepared_args);
  142.                 }
  143.         }
  144.  
  145.         /**
  146.          * @return string
  147.          */
  148.         public function error(): string {
  149.                 $err = $this->last_error;
  150.                 if ($err == null) $err = '';
  151.  
  152.                 $err = html_to_text($err); // The original ADO Exception is HTML
  153.  
  154.                 return vts_utf8_encode($err); // UTF-8 encode, because ADO might output weird stuff ...
  155.         }
  156.  
  157.         /**
  158.          * @return void
  159.          * @throws OIDplusConfigInitializationException
  160.          * @throws OIDplusException
  161.          */
  162.         protected function doConnect()/*: void*/ {
  163.                 if (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') {
  164.                         throw new OIDplusConfigInitializationException(_L('Functionality only available on Windows systems'));
  165.                 }
  166.  
  167.                 if (!class_exists('COM')) {
  168.                         throw new OIDplusConfigInitializationException(_L('To use %1, please enable the lines "extension=%2" and "extension_dir=ext" in the configuration file %3.',get_class(),'com_dotnet',php_ini_loaded_file() ? php_ini_loaded_file() : 'PHP.ini'));
  169.                 }
  170.  
  171.                 // Try connecting to the database
  172.  
  173.                 $conn = new \COM("ADODB.Connection", NULL, 65001/*CP_UTF8*/);
  174.  
  175.                 $connStr = OIDplus::baseConfig()->getValue('ADO_CONNECTION_STRING', 'Provider=MSOLEDBSQL;Data Source=LOCALHOST\SQLEXPRESS;Initial Catalog=oidplus;Integrated Security=SSPI');
  176.  
  177.                 // TODO: Nothing seems to work! Unicode characters entered in SQL Management Studio are not showing up in OIDplus
  178.                 //$connStr .=  ";Client_CSet=UTF-8;Server_CSet=Windows-1251";
  179.                 //$connStr .=  ";Client_CSet=Windows-1251;Server_CSet=UTF-8";
  180.                 //$connStr .=  ";Client_CSet=Windows-1251;Server_CSet=Windows-1251";
  181.                 //$connStr .=  ";Client_CSet=UTF-8;Server_CSet=UTF-8";
  182.                 //$connStr .= ";CharacterSet=65001";
  183.  
  184.                 try {
  185.                         if (stripos($connStr, "charset=") === false) {
  186.                                 // Try to extend DSN with charset
  187.                                 // Note: For MySQL, must be utf8 or utf8, and not UTF-8
  188.                                 try {
  189.                                         /** @phpstan-ignore-next-line */
  190.                                         $conn->Open("$connStr;charset=utf8mb4");
  191.                                         $this->conn = $conn;
  192.                                 } catch (\Exception $e1) {
  193.                                         try {
  194.                                                 /** @phpstan-ignore-next-line */
  195.                                                 $conn->Open("$connStr;charset=utf8");
  196.                                                 $this->conn = $conn;
  197.                                         } catch (\Exception $e2) {
  198.                                                 try {
  199.                                                         /** @phpstan-ignore-next-line */
  200.                                                         $conn->Open("$connStr;charset=UTF-8");
  201.                                                         $this->conn = $conn;
  202.                                                 } catch (\Exception $e3) {
  203.                                                         /** @phpstan-ignore-next-line */
  204.                                                         $conn->Open($connStr);
  205.                                                         $this->conn = $conn;
  206.                                                 }
  207.                                         }
  208.                                 }
  209.                         } else {
  210.                                 /** @phpstan-ignore-next-line */
  211.                                 $conn->Open($connStr);
  212.                                 $this->conn = $conn;
  213.                         }
  214.                 } catch (\Exception $e) {
  215.                         $message = $e->getMessage();
  216.                         $message = vts_utf8_encode($message); // Make UTF-8 if it is NOT already UTF-8. Important for German Microsoft Access.
  217.                         throw new OIDplusConfigInitializationException(trim(_L('Connection to the database failed!').' '.$message));
  218.                 }
  219.  
  220.                 $this->last_error = null;
  221.  
  222.                 try {
  223.                         /** @phpstan-ignore-next-line */
  224.                         $this->conn->Execute("SET NAMES 'UTF-8'"); // Does most likely NOT work with ADO. Try adding ";CHARSET=UTF8" (or similar) to the DSN
  225.                 } catch (\Exception $e) {
  226.                 }
  227.  
  228.                 try {
  229.                         /** @phpstan-ignore-next-line */
  230.                         $this->conn->Execute("SET CHARACTER SET 'UTF-8'"); // Does most likely NOT work with ADO. Try adding ";CHARSET=UTF8" (or similar) to the DSN
  231.                 } catch (\Exception $e) {
  232.                 }
  233.  
  234.                 try {
  235.                         /** @phpstan-ignore-next-line */
  236.                         $this->conn->Execute("SET NAMES 'utf8mb4'"); // Does most likely NOT work with ADO. Try adding ";CHARSET=UTF8" (or similar) to the DSN
  237.                 } catch (\Exception $e) {
  238.                 }
  239.         }
  240.  
  241.         /**
  242.          * @return void
  243.          */
  244.         protected function doDisconnect()/*: void*/ {
  245.                 if (!is_null($this->conn)) {
  246.                         if ($this->conn->State != 0) $this->conn->Close();
  247.                         $this->conn = null;
  248.                 }
  249.         }
  250.  
  251.         /**
  252.          * @return array
  253.          */
  254.         private function connectionProperties(): array {
  255.                 $ary = array();
  256.                 for ($i=0; $i<$this->conn->Properties->Count; $i++) {
  257.                         $ary[$this->conn->Properties->Item($i)->Name] = $this->conn->Properties->Item($i)->Value;
  258.                 }
  259.                 return $ary;
  260.         }
  261.  
  262.         /**
  263.          * @var int
  264.          */
  265.         private $trans_level = 0;
  266.  
  267.         /**
  268.          * @return bool
  269.          */
  270.         public function transaction_supported(): bool {
  271.                 // DBPROPVAL_TC_NONE 0 TAs werden nicht unterstützt
  272.                 // DBPROPVAL_TC_DML 1 TAs können nur DML ausführen. DDLs verursachen Fehler.
  273.                 // DBPROPVAL_TC_DDL_COMMIT 2 TAs können nur DML ausführen. DDLs bewirken einen COMMIT.
  274.                 // DBPROPVAL_TC_DDL_IGNORE 4 TAs können nur DML statements enthalten. DDL statements werden ignoriert.
  275.                 // DBPROPVAL_TC_ALL 8 TAs werden vollständig unterstützt.
  276.                 // DBPROPVAL_TC_DDL_LOCK 16 TAs können DML+DDL statements sein. Tabellen oder Indices erhalten bei Modifikation aber eine Lock für die Dauer der TA.
  277.                 $props = $this->connectionProperties();
  278.                 return $props['Transaction DDL'] >= 8;
  279.         }
  280.  
  281.         /**
  282.          * @return int
  283.          */
  284.         public function transaction_level(): int {
  285.                 if (!$this->transaction_supported()) {
  286.                         // TODO?
  287.                         return 0;
  288.                 }
  289.                 return $this->trans_level;
  290.         }
  291.  
  292.         /**
  293.          * @return void
  294.          * @throws OIDplusException
  295.          */
  296.         public function transaction_begin()/*: void*/ {
  297.                 if (!$this->transaction_supported()) {
  298.                         // TODO?
  299.                         return;
  300.                 }
  301.                 if ($this->trans_level > 0) throw new OIDplusException(_L('Nested transactions are not supported by this database plugin.'));
  302.                 $this->trans_level = $this->conn->BeginTrans();
  303.         }
  304.  
  305.         /**
  306.          * @return void
  307.          */
  308.         public function transaction_commit()/*: void*/ {
  309.                 if (!$this->transaction_supported()) {
  310.                         // TODO?
  311.                         return;
  312.                 }
  313.                 $this->conn->CommitTrans();
  314.                 $this->trans_level--;
  315.         }
  316.  
  317.         /**
  318.          * @return void
  319.          */
  320.         public function transaction_rollback()/*: void*/ {
  321.                 if (!$this->transaction_supported()) {
  322.                         // TODO?
  323.                         return;
  324.                 }
  325.                 $this->conn->RollbackTrans();
  326.                 $this->trans_level--;
  327.         }
  328.  
  329.         /**
  330.          * @return array
  331.          */
  332.         public function getExtendedInfo(): array {
  333.                 $props = $this->connectionProperties();
  334.                 if (isset($props['Provider Name'])) {
  335.                         // https://learn.microsoft.com/en-us/sql/connect/oledb/oledb-driver-for-sql-server?view=sql-server-ver16
  336.                         if (strtoupper($props['Provider Name']) == 'SQLOLEDB.DLL') {
  337.                                 $props['OLE DB for SQL Server Provider Generation'] = _L('Generation %1', 1);
  338.                         } else if (strtoupper($props['Provider Name']) == 'SQLNCLI11.DLL') {
  339.                                 $props['OLE DB for SQL Server Provider Generation'] = _L('Generation %1', 2);
  340.                         } else if (strtoupper($props['Provider Name']) == 'MSOLEDBSQL.DLL') {
  341.                                 $props['OLE DB for SQL Server Provider Generation'] = _L('Generation %1', 3);
  342.                         }
  343.                 }
  344.                 if (isset($props['Password'])) $props['Password'] = '['._L('redacted').']';
  345.                 return $props;
  346.         }
  347.  
  348. }
  349.