Subversion Repositories oidplus

Rev

Rev 1227 | Rev 1240 | 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 {
  60.                                 // TODO: More types?
  61.                                 if ($this->slangDetectionDone) {
  62.                                         $replace = "'".$this->getSlang()->escapeString($arg ?? '')."'";
  63.                                 } else {
  64.                                         $replace = "'".str_replace("'", "''", $arg)."'";
  65.                                 }
  66.                         }
  67.                         $pos = strpos($sql, $needle);
  68.                         if ($pos !== false) {
  69.                                 $sql = substr_replace($sql, $replace, $pos, strlen($needle));
  70.                         }
  71.                 }
  72.                 $sql = str_replace(chr(1), '?', $sql);
  73.                 return $this->doQuery($sql);
  74.         }
  75.  
  76.         /**
  77.          * @var int
  78.          */
  79.         private $rowsAffected = 0;
  80.  
  81.         /**
  82.          * @return int
  83.          */
  84.         public function rowsAffected(): int {
  85.                 return $this->rowsAffected;
  86.         }
  87.  
  88.         /**
  89.          * @param string $sql
  90.          * @param array|null $prepared_args
  91.          * @return OIDplusQueryResultADO
  92.          * @throws OIDplusException
  93.          */
  94.         public function doQuery(string $sql, array $prepared_args=null): OIDplusQueryResult {
  95.                 $this->last_error = null;
  96.                 if (is_null($prepared_args)) {
  97.                         try {
  98.                                 if (str_starts_with(trim(strtolower($sql)),'select')) {
  99.                                         $res = new \COM("ADODB.Recordset");
  100.  
  101.                                         $res->Open($sql, $this->conn, 3/*adOpenStatic*/, 3/*adLockOptimistic*/);   /** @phpstan-ignore-line */
  102.  
  103.                                         $deb = new OIDplusQueryResultADO($res);
  104.  
  105.                                         // These two lines are important, otherwise INSERT queries won't have @@ROWCOUNT and stuff...
  106.                                         // It's probably this an MARS issue (multiple result sets open at the same time),
  107.                                         // especially because the __destruct() raises an Exception that the dataset is already closed...
  108.                                         $deb->prefetchAll();
  109.                                         $res->Close(); /** @phpstan-ignore-line */
  110.  
  111.                                         // Important: Do num_rows() after prefetchAll(), because
  112.                                         // at OLE DB provider for SQL Server, RecordCount is -1 for queries
  113.                                         // which don't have physical row tables, e.g. "select max(id) as maxid from ###log"
  114.                                         // If we have prefetched the table, then RecordCount won't be checked;
  115.                                         // instead, the prefetched array will be counted.
  116.                                         $this->rowsAffected = $deb->num_rows();
  117.  
  118.                                         return $deb;
  119.  
  120.                                 } else {
  121.                                         $this->conn->Execute($sql, $this->rowsAffected);
  122.                                         return new OIDplusQueryResultADO(null);
  123.                                 }
  124.                         } catch (\Exception $e) {
  125.                                 $this->last_error = $e->getMessage();
  126.                                 throw new OIDplusSQLException($sql, $this->error());
  127.                         }
  128.  
  129.                 } else {
  130.                         return $this->doQueryPrepareEmulation($sql, $prepared_args);
  131.                 }
  132.         }
  133.  
  134.         /**
  135.          * @return string
  136.          */
  137.         public function error(): string {
  138.                 $err = $this->last_error;
  139.                 if ($err == null) $err = '';
  140.  
  141.                 $err = html_to_text($err); // The original ADO Exception is HTML
  142.  
  143.                 return vts_utf8_encode($err); // UTF-8 encode, because ADO might output weird stuff ...
  144.         }
  145.  
  146.         /**
  147.          * @return void
  148.          * @throws OIDplusConfigInitializationException
  149.          * @throws OIDplusException
  150.          */
  151.         protected function doConnect()/*: void*/ {
  152.                 if (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') {
  153.                         throw new OIDplusConfigInitializationException(_L('Functionality only available on Windows systems'));
  154.                 }
  155.  
  156.                 if (!class_exists('COM')) {
  157.                         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'));
  158.                 }
  159.  
  160.                 // Try connecting to the database
  161.  
  162.                 $conn = new \COM("ADODB.Connection", NULL, 65001/*CP_UTF8*/);
  163.  
  164.                 $connStr = OIDplus::baseConfig()->getValue('ADO_CONNECTION_STRING', 'Provider=MSOLEDBSQL;Data Source=LOCALHOST\SQLEXPRESS;Initial Catalog=oidplus;Integrated Security=SSPI');
  165.  
  166.                 // TODO: Nothing seems to work! Unicode characters entered in SQL Management Studio are not showing up in OIDplus
  167.                 //$connStr .=  ";Client_CSet=UTF-8;Server_CSet=Windows-1251";
  168.                 //$connStr .=  ";Client_CSet=Windows-1251;Server_CSet=UTF-8";
  169.                 //$connStr .=  ";Client_CSet=Windows-1251;Server_CSet=Windows-1251";
  170.                 //$connStr .=  ";Client_CSet=UTF-8;Server_CSet=UTF-8";
  171.                 //$connStr .= ";CharacterSet=65001";
  172.  
  173.                 try {
  174.                         if (stripos($connStr, "charset=") === false) {
  175.                                 // Try to extend DSN with charset
  176.                                 // Note: For MySQL, must be utf8 or utf8, and not UTF-8
  177.                                 try {
  178.                                         /** @phpstan-ignore-next-line */
  179.                                         $conn->Open("$connStr;charset=utf8mb4");
  180.                                         $this->conn = $conn;
  181.                                 } catch (\Exception $e1) {
  182.                                         try {
  183.                                                 /** @phpstan-ignore-next-line */
  184.                                                 $conn->Open("$connStr;charset=utf8");
  185.                                                 $this->conn = $conn;
  186.                                         } catch (\Exception $e2) {
  187.                                                 try {
  188.                                                         /** @phpstan-ignore-next-line */
  189.                                                         $conn->Open("$connStr;charset=UTF-8");
  190.                                                         $this->conn = $conn;
  191.                                                 } catch (\Exception $e3) {
  192.                                                         /** @phpstan-ignore-next-line */
  193.                                                         $conn->Open($connStr);
  194.                                                         $this->conn = $conn;
  195.                                                 }
  196.                                         }
  197.                                 }
  198.                         } else {
  199.                                 /** @phpstan-ignore-next-line */
  200.                                 $conn->Open($connStr);
  201.                                 $this->conn = $conn;
  202.                         }
  203.                 } catch (\Exception $e) {
  204.                         $message = $e->getMessage();
  205.                         $message = vts_utf8_encode($message); // Make UTF-8 if it is NOT already UTF-8. Important for German Microsoft Access.
  206.                         throw new OIDplusConfigInitializationException(trim(_L('Connection to the database failed!').' '.$message));
  207.                 }
  208.  
  209.                 $this->last_error = null;
  210.  
  211.                 try {
  212.                         /** @phpstan-ignore-next-line */
  213.                         $this->conn->Execute( "SET NAMES 'UTF-8'"); // Does most likely NOT work with ADO. Try adding ";CHARSET=UTF8" (or similar) to the DSN
  214.                 } catch (\Exception $e) {
  215.                 }
  216.  
  217.                 try {
  218.                         /** @phpstan-ignore-next-line */
  219.                         $this->conn->Execute("SET CHARACTER SET 'UTF-8'"); // Does most likely NOT work with ADO. Try adding ";CHARSET=UTF8" (or similar) to the DSN
  220.                 } catch (\Exception $e) {
  221.                 }
  222.  
  223.                 try {
  224.                         /** @phpstan-ignore-next-line */
  225.                         $this->conn->Execute("SET NAMES 'utf8mb4'"); // Does most likely NOT work with ADO. Try adding ";CHARSET=UTF8" (or similar) to the DSN
  226.                 } catch (\Exception $e) {
  227.                 }
  228.         }
  229.  
  230.         /**
  231.          * @return void
  232.          */
  233.         protected function doDisconnect()/*: void*/ {
  234.                 if (!is_null($this->conn)) {
  235.                         if ($this->conn->State != 0) $this->conn->Close();
  236.                         $this->conn = null;
  237.                 }
  238.         }
  239.  
  240.         /**
  241.          * @return array
  242.          */
  243.         private function connectionProperties(): array {
  244.                 $ary = array();
  245.                 for ($i=0; $i<$this->conn->Properties->Count; $i++) {
  246.                         $ary[$this->conn->Properties->Item($i)->Name] = $this->conn->Properties->Item($i)->Value;
  247.                 }
  248.                 return $ary;
  249.         }
  250.  
  251.         /**
  252.          * @var int
  253.          */
  254.         private $trans_level = 0;
  255.  
  256.         /**
  257.          * @return bool
  258.          */
  259.         public function transaction_supported(): bool {
  260.                 // DBPROPVAL_TC_NONE 0 TAs werden nicht unterstützt
  261.                 // DBPROPVAL_TC_DML 1 TAs können nur DML ausführen. DDLs verursachen Fehler.
  262.                 // DBPROPVAL_TC_DDL_COMMIT 2 TAs können nur DML ausführen. DDLs bewirken einen COMMIT.
  263.                 // DBPROPVAL_TC_DDL_IGNORE 4 TAs können nur DML statements enthalten. DDL statements werden ignoriert.
  264.                 // DBPROPVAL_TC_ALL 8 TAs werden vollständig unterstützt.
  265.                 // 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.
  266.                 $props = $this->connectionProperties();
  267.                 return $props['Transaction DDL'] >= 8;
  268.         }
  269.  
  270.         /**
  271.          * @return int
  272.          */
  273.         public function transaction_level(): int {
  274.                 if (!$this->transaction_supported()) {
  275.                         // TODO?
  276.                         return 0;
  277.                 }
  278.                 return $this->trans_level;
  279.         }
  280.  
  281.         /**
  282.          * @return void
  283.          * @throws OIDplusException
  284.          */
  285.         public function transaction_begin()/*: void*/ {
  286.                 if (!$this->transaction_supported()) {
  287.                         // TODO?
  288.                         return;
  289.                 }
  290.                 if ($this->trans_level > 0) throw new OIDplusException(_L('Nested transactions are not supported by this database plugin.'));
  291.                 $this->trans_level = $this->conn->BeginTrans();
  292.         }
  293.  
  294.         /**
  295.          * @return void
  296.          */
  297.         public function transaction_commit()/*: void*/ {
  298.                 if (!$this->transaction_supported()) {
  299.                         // TODO?
  300.                         return;
  301.                 }
  302.                 $this->conn->CommitTrans();
  303.                 $this->trans_level--;
  304.         }
  305.  
  306.         /**
  307.          * @return void
  308.          */
  309.         public function transaction_rollback()/*: void*/ {
  310.                 if (!$this->transaction_supported()) {
  311.                         // TODO?
  312.                         return;
  313.                 }
  314.                 $this->conn->RollbackTrans();
  315.                 $this->trans_level--;
  316.         }
  317.  
  318.         /**
  319.          * @return array
  320.          */
  321.         public function getExtendedInfo(): array {
  322.                 $props = $this->connectionProperties();
  323.                 if (isset($props['Provider Name'])) {
  324.                         // https://learn.microsoft.com/en-us/sql/connect/oledb/oledb-driver-for-sql-server?view=sql-server-ver16
  325.                         if (strtoupper($props['Provider Name']) == 'SQLOLEDB.DLL') {
  326.                                 $props['OLE DB for SQL Server Provider Generation'] = _L('Generation %1', 1);
  327.                         } else if (strtoupper($props['Provider Name']) == 'SQLNCLI11.DLL') {
  328.                                 $props['OLE DB for SQL Server Provider Generation'] = _L('Generation %1', 2);
  329.                         } else if (strtoupper($props['Provider Name']) == 'MSOLEDBSQL.DLL') {
  330.                                 $props['OLE DB for SQL Server Provider Generation'] = _L('Generation %1', 3);
  331.                         }
  332.                 }
  333.                 if (isset($props['Password'])) $props['Password'] = '['._L('redacted').']';
  334.                 return $props;
  335.         }
  336.  
  337. }
  338.