Subversion Repositories oidplus

Rev

Rev 1220 | Rev 1241 | 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 OIDplusDatabaseConnectionPDO extends OIDplusDatabaseConnection {
  27.         /**
  28.          * @var mixed|null
  29.          */
  30.         private $conn = null;
  31.  
  32.         /**
  33.          * @var string|null
  34.          */
  35.         private $last_error = null; // we need that because PDO divides prepared statement errors and normal query errors, but we have only one "error()" method
  36.  
  37.         /**
  38.          * @var bool
  39.          */
  40.         private $transactions_supported = false;
  41.  
  42.         /**
  43.          * @param string $sql
  44.          * @param array|null $prepared_args
  45.          * @return OIDplusQueryResultPDO
  46.          * @throws OIDplusException
  47.          */
  48.         public function doQuery(string $sql, array $prepared_args=null): OIDplusQueryResult {
  49.                 $this->last_error = null;
  50.                 if (is_null($prepared_args)) {
  51.                         $res = $this->conn->query($sql);
  52.  
  53.                         if ($res === false) {
  54.                                 $this->last_error = $this->conn->errorInfo()[2];
  55.                                 throw new OIDplusSQLException($sql, $this->error());
  56.                         } else {
  57.                                 return new OIDplusQueryResultPDO($res);
  58.                         }
  59.                 } else {
  60.                         foreach ($prepared_args as &$value) {
  61.                                 // We need to manually convert booleans into strings, because there is a
  62.                                 // 14 year old bug that hasn't been adressed by the PDO developers:
  63.                                 // https://bugs.php.net/bug.php?id=57157
  64.                                 if (is_bool($value)) {
  65.                                         if ($this->slangDetectionDone) {
  66.                                                 $value = $this->getSlang()->getSQLBool($value);
  67.                                         } else {
  68.                                                 // This works for everything except Microsoft Access (which needs -1 and 0)
  69.                                                 // Note: We are using '1' and '0' instead of 'true' and 'false' because MySQL converts boolean to tinyint(1)
  70.                                                 $value = $value ? '1' : '0';
  71.                                         }
  72.                                 }
  73.                         }
  74.  
  75.                         $ps = $this->conn->prepare($sql);
  76.                         if (!$ps) {
  77.                                 $this->last_error = $this->conn->errorInfo()[2];
  78.                                 if (!$this->last_error) $this->last_error = _L("Error")." ".$this->conn->errorInfo()[0]; // if no message is available, only show the error-code
  79.                                 throw new OIDplusSQLException($sql, _L('Cannot prepare statement').': '.$this->error());
  80.                         }
  81.  
  82.                         if (!@$ps->execute($prepared_args)) {
  83.                                 $this->last_error = $ps->errorInfo()[2];
  84.                                 if (!$this->last_error) $this->last_error = _L("Error")." ".$ps->errorInfo()[0]; // if no message is available, only show the error-code
  85.                                 // TODO:
  86.                                 // On my test machine with PDO + mysql on XAMPP with PHP 8.2.0, there are two problems with the following code:
  87.                                 //        $db->query("SELECT * from NONEXISTING", array(''));  // note that there is an additional argument, which is wrong!
  88.                                 //        $db->error()
  89.                                 // 1. $ps->errorInfo() is ['HY093', null, null].
  90.                                 //    The actual error message "Invalid parameter number: number of bound variables does not match number of tokens" is not shown via errorInfo()
  91.                                 //    => For now, as workaround, we just show the error message "HY093", if there is no driver specific error text available.
  92.                                 //       However, this means that the test-case will fail, because the table name cannot be found in the error message?!
  93.                                 // 2. The error "Invalid parameter number: number of bound variables does not match number of tokens" is SHOWN as PHP-warning
  94.                                 //    It seems like PDO::ERRMODE_SILENT is ignored?! The bug is 11 years old: https://bugs.php.net/bug.php?id=63812
  95.                                 //    => For now, as workaround, we added "@" in front of $ps->execute ...
  96.                                 //
  97.                                 // The following code works fine:
  98.                                 //        $db->query("SELECT * from NONEXISTING", array());  // note that there the number of arguments is now correct
  99.                                 //        $db->error()
  100.                                 // 1. $ps->errorInfo() is ['42S02', '1146', "Table 'oidplus.NONEXISTING' doesn't exist"].
  101.                                 //    => That's correct!
  102.                                 // 2. $ps->execute() does not show a warning (if "@" is removed)
  103.                                 //    => That's correct!
  104.  
  105.                                 throw new OIDplusSQLException($sql, $this->error());
  106.                         }
  107.                         return new OIDplusQueryResultPDO($ps);
  108.                 }
  109.         }
  110.  
  111.         /**
  112.          * @return int
  113.          * @throws OIDplusException
  114.          */
  115.         public function doInsertId(): int {
  116.                 try {
  117.                         $out = @($this->conn->lastInsertId());
  118.                         if ($out === false) return parent::doInsertId(); // fallback method that uses the SQL slang
  119.                         return $out;
  120.                 } catch (\Exception $e) {
  121.                         return parent::doInsertId(); // fallback method that uses the SQL slang
  122.                 }
  123.         }
  124.  
  125.         /**
  126.          * @return string
  127.          */
  128.         public function error(): string {
  129.                 $err = $this->last_error;
  130.                 if ($err == null) $err = '';
  131.                 return vts_utf8_encode($err);
  132.         }
  133.  
  134.         /**
  135.          * @return void
  136.          * @throws OIDplusConfigInitializationException
  137.          * @throws OIDplusException
  138.          */
  139.         protected function doConnect()/*: void*/ {
  140.                 if (!class_exists('PDO')) throw new OIDplusConfigInitializationException(_L('PHP extension "%1" not installed','PDO'));
  141.  
  142.                 try {
  143.                         $options = [
  144.                             \PDO::ATTR_ERRMODE            => \PDO::ERRMODE_SILENT,
  145.                             \PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC,
  146.                             \PDO::ATTR_EMULATE_PREPARES   => true,
  147.                         ];
  148.  
  149.                         // Try connecting to the database
  150.                         $dsn      = OIDplus::baseConfig()->getValue('PDO_DSN',      'mysql:host=localhost;dbname=oidplus;charset=utf8mb4');
  151.                         $username = OIDplus::baseConfig()->getValue('PDO_USERNAME', (str_starts_with($dsn,'odbc:')) ? '' : 'root');
  152.                         $password = OIDplus::baseConfig()->getValue('PDO_PASSWORD', '');
  153.  
  154.                         if (stripos($dsn,"charset=") === false) {
  155.                                 // Try to extend DSN with charset
  156.                                 // Note: For MySQL, must be utf8 or utf8, and not UTF-8
  157.                                 try {
  158.                                         $this->conn = new \PDO("$dsn;charset=utf8mb4", $username, $password, $options);
  159.                                 } catch (\Exception $e1) {
  160.                                         try {
  161.                                                 $this->conn = new \PDO("$dsn;charset=utf8", $username, $password, $options);
  162.                                         } catch (\Exception $e2) {
  163.                                                 try {
  164.                                                         $this->conn = new \PDO("$dsn;charset=UTF-8", $username, $password, $options);
  165.                                                 } catch (\Exception $e3) {
  166.                                                         $this->conn = new \PDO($dsn, $username, $password, $options);
  167.                                                 }
  168.                                         }
  169.                                 }
  170.                         } else {
  171.                                 $this->conn = new \PDO($dsn, $username, $password, $options);
  172.                         }
  173.                 } catch (\PDOException $e) {
  174.                         $message = $e->getMessage();
  175.                         $message = vts_utf8_encode($message); // Make UTF-8 if it is NOT already UTF-8. Important for German Microsoft Access.
  176.                         throw new OIDplusConfigInitializationException(trim(_L('Connection to the database failed!').' '.$message));
  177.                 }
  178.  
  179.                 $this->last_error = null;
  180.  
  181.                 try {
  182.                         @$this->conn->exec("SET NAMES 'utf-8'");
  183.                 } catch (\Exception $e) {
  184.                 }
  185.  
  186.                 try {
  187.                         @$this->conn->exec("SET CHARACTER SET 'utf-8'");
  188.                 } catch (\Exception $e) {
  189.                 }
  190.  
  191.                 try {
  192.                         @$this->conn->exec("SET NAMES 'utf8mb4'");
  193.                 } catch (\Exception $e) {
  194.                 }
  195.  
  196.                 // We check if the DBMS supports autocommit.
  197.                 // Attention: Check it after you have sent a query already, because Microsoft Access doesn't seem to allow
  198.                 // changing auto commit once a query was executed ("Attribute cannot be set now SQLState: S1011")
  199.                 // Note: For some weird reason we *DO* need to redirect the output to "$dummy", otherwise it won't work!
  200.                 $sql = "select name from ###config where 1=0";
  201.                 $sql = str_replace('###', OIDplus::baseConfig()->getValue('TABLENAME_PREFIX', ''), $sql);
  202.                 $dummy = $this->conn->query($sql);
  203.                 try {
  204.                         $this->conn->beginTransaction();
  205.                         $this->conn->rollBack();
  206.                         $this->transactions_supported = true;
  207.                 } catch (\Exception $e) {
  208.                         $this->transactions_supported = false;
  209.                 }
  210.         }
  211.  
  212.         /**
  213.          * @return void
  214.          */
  215.         protected function doDisconnect()/*: void*/ {
  216.                 $this->conn = null; // the connection will be closed by removing the reference
  217.         }
  218.  
  219.         /**
  220.          * @var bool
  221.          */
  222.         private $intransaction = false;
  223.  
  224.         /**
  225.          * @return bool
  226.          */
  227.         public function transaction_supported(): bool {
  228.                 return $this->transactions_supported;
  229.         }
  230.  
  231.         /**
  232.          * @return int
  233.          */
  234.         public function transaction_level(): int {
  235.                 if (!$this->transaction_supported()) {
  236.                         // TODO?
  237.                         return 0;
  238.                 }
  239.                 return $this->intransaction ? 1 : 0;
  240.         }
  241.  
  242.         /**
  243.          * @return void
  244.          * @throws OIDplusException
  245.          */
  246.         public function transaction_begin()/*: void*/ {
  247.                 if (!$this->transaction_supported()) {
  248.                         // TODO?
  249.                         return;
  250.                 }
  251.                 if ($this->intransaction) throw new OIDplusException(_L('Nested transactions are not supported by this database plugin.'));
  252.                 $this->conn->beginTransaction();
  253.                 $this->intransaction = true;
  254.         }
  255.  
  256.         /**
  257.          * @return void
  258.          */
  259.         public function transaction_commit()/*: void*/ {
  260.                 if (!$this->transaction_supported()) {
  261.                         // TODO?
  262.                         return;
  263.                 }
  264.                 $this->conn->commit();
  265.                 $this->intransaction = false;
  266.         }
  267.  
  268.         /**
  269.          * @return void
  270.          */
  271.         public function transaction_rollback()/*: void*/ {
  272.                 if (!$this->transaction_supported()) {
  273.                         // TODO?
  274.                         return;
  275.                 }
  276.                 $this->conn->rollBack();
  277.                 $this->intransaction = false;
  278.         }
  279.  
  280.         /**
  281.          * @return array
  282.          */
  283.         public function getExtendedInfo(): array {
  284.                 $dsn = OIDplus::baseConfig()->getValue('PDO_DSN', 'mysql:host=localhost;dbname=oidplus;charset=utf8mb4');
  285.                 $dsn = preg_replace('@(Password|PWD)=(.+);@ismU', '('._L('redacted').');', $dsn);
  286.                 return array(
  287.                         _L('DSN') => $dsn
  288.                 );
  289.         }
  290.  
  291. }
  292.