Subversion Repositories oidplus

Rev

Rev 502 | Rev 592 | 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 - 2021 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. if (!defined('INSIDE_OIDPLUS')) die();
  21.  
  22. abstract class OIDplusDatabaseConnection {
  23.         protected /*bool*/ $connected = false;
  24.         protected /*?bool*/ $html = null;
  25.         protected /*?string*/ $last_query = null;
  26.         protected /*bool*/ $slangDetectionDone = false;
  27.  
  28.         public abstract static function getPlugin(): OIDplusDatabasePlugin;
  29.         protected abstract function doQuery(string $sql, /*?array*/ $prepared_args=null): OIDplusQueryResult;
  30.         public abstract function error(): string;
  31.         public abstract function transaction_begin()/*: void*/;
  32.         public abstract function transaction_commit()/*: void*/;
  33.         public abstract function transaction_rollback()/*: void*/;
  34.         public abstract function transaction_supported(): bool;
  35.         public abstract function transaction_level(): int;
  36.         protected abstract function doConnect()/*: void*/;
  37.         protected abstract function doDisconnect()/*: void*/;
  38.  
  39.         public function insert_id(): int {
  40.                 // This is the "fallback" variant. If your database provider (e.g. PDO) supports
  41.                 // a function to detect the last inserted id, please override this
  42.                 // function in order to use that specialized function (since it is usually
  43.                 // more reliable).
  44.                 return $this->getSlang()->insert_id($this);
  45.         }
  46.  
  47.         public final function query(string $sql, /*?array*/ $prepared_args=null): OIDplusQueryResult {
  48.  
  49.                 $query_logfile = OIDplus::baseConfig()->getValue('QUERY_LOGFILE', '');
  50.                 if (!empty($query_logfile)) {
  51.                         $ts = explode(" ",microtime());
  52.                         $ts = date("Y-m-d H:i:s",$ts[1]).substr((string)$ts[0],1,4);
  53.                         static $log_session_id = "";
  54.                         if (empty($log_session_id)) {
  55.                                 $log_session_id = rand(10000,99999);
  56.                         }
  57.                         $file = isset($_SERVER['REQUEST_URI']) ? ' | '.$_SERVER['REQUEST_URI'] : '';
  58.                         file_put_contents($query_logfile, "$ts <$log_session_id$file> $sql\n", FILE_APPEND);
  59.                 }
  60.  
  61.                 $this->last_query = $sql;
  62.                 $sql = str_replace('###', OIDplus::baseConfig()->getValue('TABLENAME_PREFIX', ''), $sql);
  63.  
  64.                 if ($this->slangDetectionDone) {
  65.                         $slang = $this->getSlang();
  66.                         if ($slang) {
  67.                                 $sql = $slang->filterQuery($sql);
  68.                         }
  69.                 }
  70.  
  71.                 return $this->doQuery($sql, $prepared_args);
  72.         }
  73.  
  74.         public final function connect()/*: void*/ {
  75.                 if ($this->connected) return;
  76.                 $this->beforeConnect();
  77.                 $this->doConnect();
  78.                 $this->connected = true;
  79.                 register_shutdown_function(array($this, 'disconnect'));
  80.                 $this->afterConnectMandatory();
  81.                 $this->afterConnect();
  82.         }
  83.  
  84.         public final function disconnect()/*: void*/ {
  85.                 if (!$this->connected) return;
  86.                 $this->beforeDisconnect();
  87.                 $this->doDisconnect();
  88.                 $this->connected = false;
  89.                 $this->afterDisconnect();
  90.         }
  91.  
  92.         public function natOrder($fieldname, $order='asc'): string {
  93.                 $slang = $this->getSlang();
  94.                 if (!is_null($slang)) {
  95.                         return $slang->natOrder($fieldname, $order);
  96.                 } else {
  97.                         $order = strtolower($order);
  98.                         if (($order != 'asc') && ($order != 'desc')) {
  99.                                 throw new OIDplusException(_L('Invalid order "%1" (needs to be "asc" or "desc")',$order));
  100.                         }
  101.  
  102.                         // For (yet) unsupported DBMS, we do not offer natural sort
  103.                         return "$fieldname $order";
  104.                 }
  105.         }
  106.  
  107.         protected function beforeDisconnect()/*: void*/ {}
  108.  
  109.         protected function afterDisconnect()/*: void*/ {}
  110.  
  111.         protected function beforeConnect()/*: void*/ {}
  112.  
  113.         protected function afterConnect()/*: void*/ {}
  114.  
  115.         private function afterConnectMandatory()/*: void*/ {
  116.                 // Check if the config table exists. This is important because the database version is stored in it
  117.                 $this->initRequireTables(array('config'));
  118.  
  119.                 // Do the database tables need an update?
  120.                 // It is important that we do it immediately after connecting,
  121.                 // because the database structure might change and therefore various things might fail.
  122.                 // Note: The config setting "database_version" is inserted in setup/sql/...sql, not in the OIDplus core init
  123.  
  124.                 $res = $this->query("SELECT value FROM ###config WHERE name = 'database_version'");
  125.                 $row = $res->fetch_array();
  126.                 if ($row == null) {
  127.                         throw new OIDplusConfigInitializationException(_L('Cannot determine database version (the entry "database_version" inside the table "###config" is probably missing)'));
  128.                 }
  129.                 $version = $row['value'];
  130.                 if (!is_numeric($version) || ($version < 200) || ($version > 999)) {
  131.                         throw new OIDplusConfigInitializationException(_L('Entry "database_version" inside the table "###config" seems to be wrong (expect number between 200 and 999)'));
  132.                 }
  133.  
  134.                 $update_files = glob(OIDplus::localpath().'includes/db_updates/update*.inc.php');
  135.                 foreach ($update_files as $update_file) {
  136.                         include_once $update_file;
  137.                 }
  138.                 while (function_exists($function_name = "oidplus_dbupdate_".$version."_".($version+1))) {
  139.                         $prev_version = $version;
  140.                         $function_name($this, $version);
  141.                         if ($version != $prev_version+1) {
  142.                                 // This should usually not happen, since the update-file should increase the version
  143.                                 // or throw an Exception by itself
  144.                                 throw new OIDplusException(_L('Database update %1 -> %2 failed (script reports new version to be %3)',$prev_version,$prev_version+1,$version));
  145.                         }
  146.                 }
  147.  
  148.                 // Now that our database is up-to-date, we check if database tables are existing
  149.                 // without config table, because it was checked above
  150.                 $this->initRequireTables(array('objects', 'asn1id', 'iri', 'ra'/*, 'config'*/));
  151.  
  152.                 // In case an auto-detection of the slang is required (for generic providers like PDO or ODBC),
  153.                 // we must not be inside a transaction, because the detection requires intentionally submitting
  154.                 // invalid queries to detect the correct DBMS. If we would be inside a transaction, providers like
  155.                 // PDO would automatically roll-back. Therefore, we detect the slang right at the beginning,
  156.                 // before any transaction is used.
  157.                 $this->getSlang();
  158.         }
  159.  
  160.         private function initRequireTables($tableNames)/*: void*/ {
  161.                 $msgs = array();
  162.                 foreach ($tableNames as $tableName) {
  163.                         $prefix = OIDplus::baseConfig()->getValue('TABLENAME_PREFIX', '');
  164.                         if (!$this->tableExists($prefix.$tableName)) {
  165.                                 $msgs[] = _L('Table %1 is missing!',$prefix.$tableName);
  166.                         }
  167.                 }
  168.                 if (count($msgs) > 0) {
  169.                         throw new OIDplusConfigInitializationException(implode("\n\n",$msgs));
  170.                 }
  171.         }
  172.  
  173.         public function tableExists($tableName): bool {
  174.                 try {
  175.                         // Attention: This query could interrupt transactions if Rollback-On-Error is enabled
  176.                         $this->query("select 0 from ".$tableName." where 1=0");
  177.                         return true;
  178.                 } catch (Exception $e) {
  179.                         return false;
  180.                 }
  181.         }
  182.  
  183.         public function isConnected(): bool {
  184.                 return $this->connected;
  185.         }
  186.  
  187.         public function init($html = true)/*: void*/ {
  188.                 $this->html = $html;
  189.         }
  190.  
  191.         public function sqlDate(): string {
  192.                 $slang = $this->getSlang();
  193.                 if (!is_null($slang)) {
  194.                         return $slang->sqlDate();
  195.                 } else {
  196.                         return "'" . date('Y-m-d H:i:s') . "'";
  197.                 }
  198.         }
  199.  
  200.         protected function doGetSlang(bool $mustExist=true)/*: ?OIDplusSqlSlangPlugin*/ {
  201.                 $res = null;
  202.  
  203.                 if (OIDplus::baseConfig()->exists('FORCE_DBMS_SLANG')) {
  204.                         $name = OIDplus::baseConfig()->getValue('FORCE_DBMS_SLANG', '');
  205.                         $res = OIDplus::getSqlSlangPlugin($name);
  206.                         if ($mustExist && is_null($res)) {
  207.                                 throw new OIDplusConfigInitializationException(_L('Enforced SQL slang (via setting FORCE_DBMS_SLANG) "%1" does not exist.',$name));
  208.                         }
  209.                 } else {
  210.                         foreach (OIDplus::getSqlSlangPlugins() as $plugin) {
  211.                                 if ($plugin->detect($this)) {
  212.                                         if (OIDplus::baseConfig()->getValue('DEBUG') && !is_null($res)) {
  213.                                                 throw new OIDplusException(_L('DB-Slang detection failed: Multiple slangs were detected. Use base config setting FORCE_DBMS_SLANG to define one.'));
  214.                                         }
  215.  
  216.                                         $res = $plugin;
  217.  
  218.                                         if (!OIDplus::baseConfig()->getValue('DEBUG')) {
  219.                                                 break;
  220.                                         }
  221.                                 }
  222.                         }
  223.                         if ($mustExist && is_null($res)) {
  224.                                 throw new OIDplusException(_L('Cannot determine the SQL slang of your DBMS. Your DBMS is probably not supported.'));
  225.                         }
  226.                 }
  227.  
  228.                 return $res;
  229.         }
  230.  
  231.         public final function getSlang(bool $mustExist=true)/*: ?OIDplusSqlSlangPlugin*/ {
  232.                 static /*?OIDplusSqlSlangPlugin*/ $slangCache = null;
  233.  
  234.                 if ($this->slangDetectionDone) {
  235.                         return $slangCache;
  236.                 }
  237.  
  238.                 $slangCache = $this->doGetSlang();
  239.                 $this->slangDetectionDone = true;
  240.                 return $slangCache;
  241.         }
  242. }
  243.