Subversion Repositories oidplus

Rev

Rev 1086 | Rev 1130 | 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. abstract class OIDplusDatabaseConnection extends OIDplusBaseClass {
  27.         protected /*bool*/ $connected = false;
  28.         protected /*?bool*/ $html = null;
  29.         protected /*?string*/ $last_query = null;
  30.         protected /*bool*/ $slangDetectionDone = false;
  31.  
  32.         /**
  33.          * @param string $sql
  34.          * @param array|null $prepared_args
  35.          * @return OIDplusQueryResult
  36.          * @throws OIDplusException
  37.          */
  38.         protected abstract function doQuery(string $sql, array $prepared_args=null): OIDplusQueryResult;
  39.  
  40.         /**
  41.          * @return string
  42.          */
  43.         public abstract function error(): string;
  44.  
  45.         /**
  46.          * @return void
  47.          */
  48.         public abstract function transaction_begin()/*: void*/;
  49.  
  50.         /**
  51.          * @return void
  52.          */
  53.         public abstract function transaction_commit()/*: void*/;
  54.  
  55.         /**
  56.          * @return void
  57.          */
  58.         public abstract function transaction_rollback()/*: void*/;
  59.  
  60.         /**
  61.          * @return bool
  62.          */
  63.         public abstract function transaction_supported(): bool;
  64.  
  65.         /**
  66.          * @return int
  67.          */
  68.         public abstract function transaction_level(): int;
  69.  
  70.         /**
  71.          * @return void
  72.          */
  73.         protected abstract function doConnect()/*: void*/;
  74.  
  75.         /**
  76.          * @return void
  77.          */
  78.         protected abstract function doDisconnect()/*: void*/;
  79.  
  80.         /**
  81.          * @return OIDplusDatabasePlugin|null
  82.          */
  83.         public function getPlugin()/*: ?OIDplusDatabasePlugin*/ {
  84.                 $res = null;
  85.                 $plugins = OIDplus::getDatabasePlugins();
  86.                 foreach ($plugins as $plugin) {
  87.                         if (get_class($this) == get_class($plugin::newConnection())) {
  88.                                 return $plugin;
  89.                         }
  90.                 }
  91.                 return $res;
  92.         }
  93.  
  94.         /**
  95.          * @return int
  96.          * @throws OIDplusException
  97.          */
  98.         public function insert_id(): int {
  99.                 // This is the "fallback" variant. If your database provider (e.g. PDO) supports
  100.                 // a function to detect the last inserted id, please override this
  101.                 // function in order to use that specialized function (since it is usually
  102.                 // more reliable).
  103.                 return $this->getSlang()->insert_id($this);
  104.         }
  105.  
  106.         /**
  107.          * @param string $sql
  108.          * @return array[]
  109.          * @throws OIDplusException
  110.          */
  111.         public final function getTable(string $sql): array {
  112.                 $out = array();
  113.                 $res = $this->query($sql);
  114.                 while ($row = $res->fetch_array()) {
  115.                         $out[] = $row;
  116.                 }
  117.                 return $out;
  118.         }
  119.  
  120.         /**
  121.          * @param string $sql
  122.          * @return mixed|null
  123.          * @throws OIDplusException
  124.          */
  125.         public final function getScalar(string $sql) {
  126.                 $res = $this->query($sql);
  127.                 $row = $res->fetch_array();
  128.                 return $row ? reset($row) : null;
  129.         }
  130.  
  131.         /**
  132.          * @param string $sql
  133.          * @param array|null $prepared_args
  134.          * @return OIDplusQueryResult
  135.          * @throws OIDplusException
  136.          */
  137.         public final function query(string $sql, /*?array*/ $prepared_args=null): OIDplusQueryResult {
  138.  
  139.                 $query_logfile = OIDplus::baseConfig()->getValue('QUERY_LOGFILE', '');
  140.                 if (!empty($query_logfile)) {
  141.                         $ts = explode(" ",microtime());
  142.                         $ts = date("Y-m-d H:i:s",intval($ts[1])).substr((string)$ts[0],1,4);
  143.                         static $log_session_id = "";
  144.                         if (empty($log_session_id)) {
  145.                                 $log_session_id = rand(10000,99999);
  146.                         }
  147.                         $file = isset($_SERVER['REQUEST_URI']) ? ' | '.$_SERVER['REQUEST_URI'] : '';
  148.                         file_put_contents($query_logfile, "$ts <$log_session_id$file> $sql\n", FILE_APPEND);
  149.                 }
  150.  
  151.                 $this->last_query = $sql;
  152.                 $sql = str_replace('###', OIDplus::baseConfig()->getValue('TABLENAME_PREFIX', ''), $sql);
  153.  
  154.                 if ($this->slangDetectionDone) {
  155.                         $slang = $this->getSlang();
  156.                         if ($slang) {
  157.                                 $sql = $slang->filterQuery($sql);
  158.                         }
  159.                 }
  160.  
  161.                 return $this->doQuery($sql, $prepared_args);
  162.         }
  163.  
  164.         /**
  165.          * @return void
  166.          * @throws OIDplusException
  167.          */
  168.         public final function connect()/*: void*/ {
  169.                 if ($this->connected) return;
  170.                 $this->beforeConnect();
  171.                 $this->doConnect();
  172.                 $this->connected = true;
  173.                 OIDplus::register_shutdown_function(array($this, 'disconnect'));
  174.                 $this->afterConnectMandatory();
  175.                 $this->afterConnect();
  176.         }
  177.  
  178.         /**
  179.          * @return void
  180.          */
  181.         public final function disconnect()/*: void*/ {
  182.                 if (!$this->connected) return;
  183.                 $this->beforeDisconnect();
  184.                 $this->doDisconnect();
  185.                 $this->connected = false;
  186.                 $this->afterDisconnect();
  187.         }
  188.  
  189.         /**
  190.          * @param string $fieldname
  191.          * @param string $order
  192.          * @return string
  193.          * @throws OIDplusException
  194.          */
  195.         public function natOrder(string $fieldname, string $order='asc'): string {
  196.                 $slang = $this->getSlang();
  197.                 if (!is_null($slang)) {
  198.                         return $slang->natOrder($fieldname, $order);
  199.                 } else {
  200.                         $order = strtolower($order);
  201.                         if (($order != 'asc') && ($order != 'desc')) {
  202.                                 throw new OIDplusException(_L('Invalid order "%1" (needs to be "asc" or "desc")',$order));
  203.                         }
  204.  
  205.                         // For (yet) unsupported DBMS, we do not offer natural sort
  206.                         return "$fieldname $order";
  207.                 }
  208.         }
  209.  
  210.         /**
  211.          * @return void
  212.          */
  213.         protected function beforeDisconnect()/*: void*/ {}
  214.  
  215.         /**
  216.          * @return void
  217.          */
  218.         protected function afterDisconnect()/*: void*/ {}
  219.  
  220.         /**
  221.          * @return void
  222.          */
  223.         protected function beforeConnect()/*: void*/ {}
  224.  
  225.         /**
  226.          * @return void
  227.          */
  228.         protected function afterConnect()/*: void*/ {}
  229.  
  230.         /**
  231.          * @return void
  232.          * @throws OIDplusConfigInitializationException
  233.          * @throws OIDplusException
  234.          */
  235.         private function afterConnectMandatory()/*: void*/ {
  236.                 // Check if the config table exists. This is important because the database version is stored in it
  237.                 $this->initRequireTables(array('config'));
  238.  
  239.                 // Do the database tables need an update?
  240.                 // It is important that we do it immediately after connecting,
  241.                 // because the database structure might change and therefore various things might fail.
  242.                 require_once __DIR__.'/../db_updates/run.inc.php';
  243.                 oidplus_dbupdate($this);
  244.  
  245.                 // Now that our database is up-to-date, we check if database tables are existing
  246.                 // without config table, because it was checked above
  247.                 $this->initRequireTables(array('objects', 'asn1id', 'iri', 'ra'/*, 'config'*/));
  248.  
  249.                 // In case an auto-detection of the slang is required (for generic providers like PDO or ODBC),
  250.                 // we must not be inside a transaction, because the detection requires intentionally submitting
  251.                 // invalid queries to detect the correct DBMS. If we would be inside a transaction, providers like
  252.                 // PDO would automatically roll-back. Therefore, we detect the slang right at the beginning,
  253.                 // before any transaction is used.
  254.                 $this->getSlang();
  255.         }
  256.  
  257.         /**
  258.          * @param string[] $tableNames
  259.          * @return void
  260.          * @throws OIDplusConfigInitializationException
  261.          * @throws OIDplusException
  262.          */
  263.         private function initRequireTables(array $tableNames)/*: void*/ {
  264.                 $msgs = array();
  265.                 foreach ($tableNames as $tableName) {
  266.                         $prefix = OIDplus::baseConfig()->getValue('TABLENAME_PREFIX', '');
  267.                         if (!$this->tableExists($prefix.$tableName)) {
  268.                                 $msgs[] = _L('Table %1 is missing!',$prefix.$tableName);
  269.                         }
  270.                 }
  271.                 if (count($msgs) > 0) {
  272.                         throw new OIDplusConfigInitializationException(implode("\n\n",$msgs));
  273.                 }
  274.         }
  275.  
  276.         /**
  277.          * @param string $tableName
  278.          * @return bool
  279.          */
  280.         public function tableExists(string $tableName): bool {
  281.                 try {
  282.                         // Attention: This query could interrupt transactions if Rollback-On-Error is enabled
  283.                         $this->query("select 0 from ".$tableName." where 1=0");
  284.                         return true;
  285.                 } catch (\Exception $e) {
  286.                         return false;
  287.                 }
  288.         }
  289.  
  290.         /**
  291.          * @return bool
  292.          */
  293.         public function isConnected(): bool {
  294.                 return $this->connected;
  295.         }
  296.  
  297.         /**
  298.          * @param bool $html
  299.          * @return void
  300.          */
  301.         public function init(bool $html = true)/*: void*/ {
  302.                 $this->html = $html;
  303.         }
  304.  
  305.         /**
  306.          * @return string
  307.          * @throws OIDplusException
  308.          */
  309.         public function sqlDate(): string {
  310.                 $slang = $this->getSlang();
  311.                 if (!is_null($slang)) {
  312.                         return $slang->sqlDate();
  313.                 } else {
  314.                         return "'" . date('Y-m-d H:i:s') . "'";
  315.                 }
  316.         }
  317.  
  318.         /**
  319.          * @param bool $mustExist
  320.          * @return OIDplusSqlSlangPlugin|null
  321.          * @throws OIDplusConfigInitializationException
  322.          * @throws OIDplusException
  323.          */
  324.         protected function doGetSlang(bool $mustExist=true)/*: ?OIDplusSqlSlangPlugin*/ {
  325.                 $res = null;
  326.  
  327.                 if (OIDplus::baseConfig()->exists('FORCE_DBMS_SLANG')) {
  328.                         $name = OIDplus::baseConfig()->getValue('FORCE_DBMS_SLANG', '');
  329.                         $res = OIDplus::getSqlSlangPlugin($name);
  330.                         if ($mustExist && is_null($res)) {
  331.                                 throw new OIDplusConfigInitializationException(_L('Enforced SQL slang (via setting FORCE_DBMS_SLANG) "%1" does not exist.',$name));
  332.                         }
  333.                 } else {
  334.                         foreach (OIDplus::getSqlSlangPlugins() as $plugin) {
  335.                                 if ($plugin->detect($this)) {
  336.                                         if (OIDplus::baseConfig()->getValue('DEBUG') && !is_null($res)) {
  337.                                                 throw new OIDplusException(_L('DB-Slang detection failed: Multiple slangs were detected. Use base config setting FORCE_DBMS_SLANG to define one.'));
  338.                                         }
  339.  
  340.                                         $res = $plugin;
  341.  
  342.                                         if (!OIDplus::baseConfig()->getValue('DEBUG')) {
  343.                                                 break;
  344.                                         }
  345.                                 }
  346.                         }
  347.                         if ($mustExist && is_null($res)) {
  348.                                 throw new OIDplusException(_L('Cannot determine the SQL slang of your DBMS. Your DBMS is probably not supported.'));
  349.                         }
  350.                 }
  351.  
  352.                 return $res;
  353.         }
  354.  
  355.         /**
  356.          * @param bool $mustExist
  357.          * @return OIDplusSqlSlangPlugin|null
  358.          * @throws OIDplusConfigInitializationException
  359.          * @throws OIDplusException
  360.          */
  361.         public final function getSlang(bool $mustExist=true)/*: ?OIDplusSqlSlangPlugin*/ {
  362.                 static /*?OIDplusSqlSlangPlugin*/ $slangCache = null;
  363.  
  364.                 if ($this->slangDetectionDone) {
  365.                         return $slangCache;
  366.                 }
  367.  
  368.                 $slangCache = $this->doGetSlang();
  369.                 $this->slangDetectionDone = true;
  370.                 return $slangCache;
  371.         }
  372. }
  373.