Subversion Repositories oidplus

Rev

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