Subversion Repositories oidplus

Rev

Rev 1220 | 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, null);
  74.         }
  75.  
  76.         /**
  77.          * @param string $sql
  78.          * @param array|null $prepared_args
  79.          * @return OIDplusQueryResultADO
  80.          * @throws OIDplusException
  81.          */
  82.         public function doQuery(string $sql, array $prepared_args=null): OIDplusQueryResult {
  83.                 $this->last_error = null;
  84.                 if (is_null($prepared_args)) {
  85.                         $res = new \COM("ADODB.Recordset");
  86.  
  87.                         try {
  88.                                 /** @phpstan-ignore-next-line */
  89.                                 $res->Open($sql, $this->conn, 3, 3);  // adOpenStatic, adLockOptimistic
  90.                         } catch (\Exception $e) {
  91.                                 $this->last_error = $e->getMessage();
  92.                                 throw new OIDplusSQLException($sql, $this->error());
  93.                         }
  94.  
  95.                         /** @phpstan-ignore-next-line */
  96.                         if ($res->State == 0) {
  97.                                 // It was an INSERT or UPDATE command (i.e. dataset is closed now)
  98.                                 return new OIDplusQueryResultADO(true);
  99.                         } else {
  100.                                 return new OIDplusQueryResultADO($res);
  101.                         }
  102.                 } else {
  103.                         return $this->doQueryPrepareEmulation($sql, $prepared_args);
  104.                 }
  105.         }
  106.  
  107.         /**
  108.          * @return string
  109.          */
  110.         public function error(): string {
  111.                 $err = $this->last_error;
  112.                 if ($err == null) $err = '';
  113.  
  114.                 $err = html_to_text($err); // The original ADO Exception is HTML
  115.  
  116.                 return vts_utf8_encode($err); // UTF-8 encode, because ADO might output weird stuff ...
  117.         }
  118.  
  119.         /**
  120.          * @return void
  121.          * @throws OIDplusConfigInitializationException
  122.          * @throws OIDplusException
  123.          */
  124.         protected function doConnect()/*: void*/ {
  125.                 if (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') {
  126.                         throw new OIDplusConfigInitializationException(_L('Functionality only available on Windows systems'));
  127.                 }
  128.  
  129.                 if (!class_exists('COM')) {
  130.                         throw new OIDplusConfigInitializationException(_L('To use %1, please enable the lines "extension=%2" and "extension_dir=ext" in your PHP.ini file.',get_class(),'com_dotnet'));
  131.                 }
  132.  
  133.                 // Try connecting to the database
  134.  
  135.                 $conn = new \COM("ADODB.Connection", NULL, 65001/*CP_UTF8*/);
  136.  
  137.                 $connStr = OIDplus::baseConfig()->getValue('ADO_CONNECTION_STRING', 'DRIVER={SQL Server};SERVER=LOCALHOST\SQLEXPRESS;DATABASE=oidplus;CHARSET=UTF8');
  138.  
  139.                 try {
  140.                         if (stripos($connStr, "charset=") === false) {
  141.                                 // Try to extend DSN with charset
  142.                                 // Note: For MySQL, must be utf8 or utf8, and not UTF-8
  143.                                 try {
  144.                                         /** @phpstan-ignore-next-line */
  145.                                         $conn->Open("$connStr;charset=utf8mb4");
  146.                                         $this->conn = $conn;
  147.                                 } catch (\Exception $e1) {
  148.                                         try {
  149.                                                 /** @phpstan-ignore-next-line */
  150.                                                 $conn->Open("$connStr;charset=utf8");
  151.                                                 $this->conn = $conn;
  152.                                         } catch (\Exception $e2) {
  153.                                                 try {
  154.                                                         /** @phpstan-ignore-next-line */
  155.                                                         $conn->Open("$connStr;charset=UTF-8");
  156.                                                         $this->conn = $conn;
  157.                                                 } catch (\Exception $e3) {
  158.                                                         /** @phpstan-ignore-next-line */
  159.                                                         $conn->Open($connStr);
  160.                                                         $this->conn = $conn;
  161.                                                 }
  162.                                         }
  163.                                 }
  164.                         } else {
  165.                                 /** @phpstan-ignore-next-line */
  166.                                 $conn->Open($connStr);
  167.                                 $this->conn = $conn;
  168.                         }
  169.                 } catch (\Exception $e) {
  170.                         $message = $e->getMessage();
  171.                         $message = vts_utf8_encode($message); // Make UTF-8 if it is NOT already UTF-8. Important for German Microsoft Access.
  172.                         throw new OIDplusConfigInitializationException(trim(_L('Connection to the database failed!').' '.$message));
  173.                 }
  174.  
  175.                 $this->last_error = null;
  176.  
  177.                 try {
  178.                         /** @phpstan-ignore-next-line */
  179.                         $this->conn->Execute( "SET NAMES 'UTF-8'"); // Does most likely NOT work with ADO. Try adding ";CHARSET=UTF8" (or similar) to the DSN
  180.                 } catch (\Exception $e) {
  181.                 }
  182.  
  183.                 try {
  184.                         /** @phpstan-ignore-next-line */
  185.                         $this->conn->Execute("SET CHARACTER SET 'UTF-8'"); // Does most likely NOT work with ADO. Try adding ";CHARSET=UTF8" (or similar) to the DSN
  186.                 } catch (\Exception $e) {
  187.                 }
  188.  
  189.                 try {
  190.                         /** @phpstan-ignore-next-line */
  191.                         $this->conn->Execute("SET NAMES 'utf8mb4'"); // Does most likely NOT work with ADO. Try adding ";CHARSET=UTF8" (or similar) to the DSN
  192.                 } catch (\Exception $e) {
  193.                 }
  194.         }
  195.  
  196.         /**
  197.          * @return void
  198.          */
  199.         protected function doDisconnect()/*: void*/ {
  200.                 if (!is_null($this->conn)) {
  201.                         try {
  202.                                 $this->conn->Close();
  203.                         } catch (\Exception $e) {
  204.                                 // For some reason, in test_database_plugins.php (tested with ODBC-MSSQL), the disconnection method raises the Exception (TODO?)
  205.                                 //    Source: ADODB.Recordset
  206.                                 //    Description: Der Vorgang ist fuer ein geschlossenes Objekt nicht zugelassen.
  207.                         }
  208.                         $this->conn = null;
  209.                 }
  210.         }
  211.  
  212.         /**
  213.          * @return array
  214.          */
  215.         private function connectionProperties(): array {
  216.                 $ary = array();
  217.                 for ($i=0; $i<$this->conn->Properties->Count; $i++) {
  218.                         $ary[$this->conn->Properties->Item($i)->Name] = $this->conn->Properties->Item($i)->Value;
  219.                 }
  220.                 return $ary;
  221.         }
  222.  
  223.         /**
  224.          * @var int
  225.          */
  226.         private $trans_level = 0;
  227.  
  228.         /**
  229.          * @return bool
  230.          */
  231.         public function transaction_supported(): bool {
  232.                 // DBPROPVAL_TC_NONE 0 TAs werden nicht unterstützt
  233.                 // DBPROPVAL_TC_DML 1 TAs können nur DML ausführen. DDLs verursachen Fehler.
  234.                 // DBPROPVAL_TC_DDL_COMMIT 2 TAs können nur DML ausführen. DDLs bewirken einen COMMIT.
  235.                 // DBPROPVAL_TC_DDL_IGNORE 4 TAs können nur DML statements enthalten. DDL statements werden ignoriert.
  236.                 // DBPROPVAL_TC_ALL 8 TAs werden vollständig unterstützt.
  237.                 // 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.
  238.                 $props = $this->connectionProperties();
  239.                 return $props['Transaction DDL'] >= 8;
  240.         }
  241.  
  242.         /**
  243.          * @return int
  244.          */
  245.         public function transaction_level(): int {
  246.                 if (!$this->transaction_supported()) {
  247.                         // TODO?
  248.                         return 0;
  249.                 }
  250.                 return $this->trans_level;
  251.         }
  252.  
  253.         /**
  254.          * @return void
  255.          * @throws OIDplusException
  256.          */
  257.         public function transaction_begin()/*: void*/ {
  258.                 if (!$this->transaction_supported()) {
  259.                         // TODO?
  260.                         return;
  261.                 }
  262.                 if ($this->trans_level > 0) throw new OIDplusException(_L('Nested transactions are not supported by this database plugin.'));
  263.                 $this->trans_level = $this->conn->BeginTrans();
  264.         }
  265.  
  266.         /**
  267.          * @return void
  268.          */
  269.         public function transaction_commit()/*: void*/ {
  270.                 if (!$this->transaction_supported()) {
  271.                         // TODO?
  272.                         return;
  273.                 }
  274.                 $this->conn->CommitTrans();
  275.                 $this->trans_level--;
  276.         }
  277.  
  278.         /**
  279.          * @return void
  280.          */
  281.         public function transaction_rollback()/*: void*/ {
  282.                 if (!$this->transaction_supported()) {
  283.                         // TODO?
  284.                         return;
  285.                 }
  286.                 $this->conn->RollbackTrans();
  287.                 $this->trans_level--;
  288.         }
  289. }
  290.