Subversion Repositories oidplus

Rev

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