TimeTrex/includes/Database.inc.php

199 lines
13 KiB
PHP
Raw Permalink Normal View History

2022-12-13 07:10:06 +01:00
<?php /** @noinspection PhpUndefinedVariableInspection */
/** @noinspection PhpUndefinedFunctionInspection */
/** @noinspection ALL */
/*********************************************************************************
*
* TimeTrex is a Workforce Management program developed by
* TimeTrex Software Inc. Copyright (C) 2003 - 2021 TimeTrex Software Inc.
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License version 3 as published by
* the Free Software Foundation with the addition of the following permission
* added to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED
* WORK IN WHICH THE COPYRIGHT IS OWNED BY TIMETREX, TIMETREX DISCLAIMS THE
* WARRANTY OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
*
* You should have received a copy of the GNU Affero General Public License along
* with this program; if not, see http://www.gnu.org/licenses or write to the Free
* Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301 USA.
*
*
* You can contact TimeTrex headquarters at Unit 22 - 2475 Dobbin Rd. Suite
* #292 West Kelowna, BC V4T 2E9, Canada or at email address info@timetrex.com.
*
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
*
* In accordance with Section 7(b) of the GNU Affero General Public License
* version 3, these Appropriate Legal Notices must retain the display of the
* "Powered by TimeTrex" logo. If the display of the logo is not reasonably
* feasible for technical reasons, the Appropriate Legal Notices must display
* the words "Powered by TimeTrex".
*
********************************************************************************/
define( 'ADODB_DEFAULT_METATYPE', 'C' ); //Default to "C" or char type, which should allow most advanced data types such as geometry/ENUMS/JSON to work without further modifications.
require_once( Environment::getBasePath() . 'classes' . DIRECTORY_SEPARATOR . 'adodb' . DIRECTORY_SEPARATOR . 'adodb.inc.php' );
require_once( Environment::getBasePath() . 'classes' . DIRECTORY_SEPARATOR . 'adodb' . DIRECTORY_SEPARATOR . 'adodb-exceptions.inc.php' );
$PRIMARY_KEY_IS_UUID = true;
$COMPANY_GENERIC_MAP_DELETED_COLUMN = true; //Schema 1126A added "deleted" column to company_generic_map table, so we have to not use it in SELECT queries prior to 1126A upgrade.
//Use overloading to abstract $db and have calls directly to ADODB
if ( !isset( $disable_database_connection ) ) {
if ( isset( $config_vars['database']['type'] ) && isset( $config_vars['database']['host'] ) && isset( $config_vars['database']['user'] ) && isset( $config_vars['database']['password'] ) && isset( $config_vars['database']['database_name'] ) ) {
try {
if ( isset( $config_vars['cache']['dir'] ) && $config_vars['cache']['dir'] != '' ) {
$ADODB_CACHE_DIR = $config_vars['cache']['dir'] . DIRECTORY_SEPARATOR;
}
if ( Debug::getVerbosity() >= 12 ) {
$ADODB_OUTP = 'ADODBDebug';
function ADODBDebug( $msg, $newline = true ) {
Debug::Text( html_entity_decode( strip_tags( $msg ) ), __FILE__, __LINE__, __METHOD__, 11 );
return true;
}
}
if ( !isset( $config_vars['database']['persistent_connections'] ) ) {
$config_vars['database']['persistent_connections'] = false;
}
//Always force the database type that starts with "postgres" (ie: postgres7, postgres8, ...) to be just 'postgres'.
if ( strpos( $config_vars['database']['type'], 'postgres' ) !== false ) {
$config_vars['database']['type'] = 'postgres';
}
$ADODB_GETONE_EOF = false; //Make sure GetOne returns FALSE rather then NULL.
if ( strpos( $config_vars['database']['host'], ',' ) !== false ) {
require_once( Environment::getBasePath() . 'classes' . DIRECTORY_SEPARATOR . 'adodb' . DIRECTORY_SEPARATOR . 'adodb-loadbalancer.inc.php' );
$db = new ADOdbLoadBalancer();
//Use comma separated database hosts, assuming the first is always the master, the rest are slaves.
//Anything after the # is the weight. Username/password/database is assumed to be the same across all connections.
//ie: 127.0.0.1:5433#10,127.0.0.2:5433#100,127.0.0.3:5433#120
$db_hosts = Misc::parseDatabaseHostString( $config_vars['database']['host'] );
foreach ( $db_hosts as $db_host_arr ) {
Debug::Text( 'Adding DB Connection: Host: ' . $db_host_arr[0] . ' Type: ' . $db_host_arr[1] . ' Weight: ' . $db_host_arr[2], __FILE__, __LINE__, __METHOD__, 1 );
$db_connection_obj = new ADOdbLoadBalancerConnection( $config_vars['database']['type'], $db_host_arr[1], $db_host_arr[2], (bool)$config_vars['database']['persistent_connections'], $db_host_arr[0], $config_vars['database']['user'], $config_vars['database']['password'], $config_vars['database']['database_name'] );
$db_connection_obj->setConnectionTestCallback( function( $connection_obj, $adodb_obj ) use ( $config_vars ) {
if ( $connection_obj->type == 'readonly' ) {
//When connecting to a slave database, make sure the replication delay never exceeds our threshold, otherwise discard the connection and try a different host.
$maximum_replication_delay = 60; //Seconds
if ( isset($config_vars['database']['maximum_replication_delay']) && is_numeric( $config_vars['database']['maximum_replication_delay'] ) ) {
$maximum_replication_delay = (float)$config_vars['database']['maximum_replication_delay'];
}
$result = (float)$adodb_obj->GetOne( 'SELECT EXTRACT(epoch FROM ( now() - CASE WHEN pg_last_xact_replay_timestamp() IS NULL THEN now() ELSE pg_last_xact_replay_timestamp() END ) ) AS replication_delay' );
//Debug::Text( ' Database Replication Delay: '. $result .' Host: '. $connection_obj->host, __FILE__, __LINE__, __METHOD__, 10 );
if ( $result <= $maximum_replication_delay ) {
return true;
} else {
Debug::Text( 'ERROR: Database Connection is invalid, ignoring! Host: '. $connection_obj->host .' Replication Delay: '. $result, __FILE__, __LINE__, __METHOD__, 1 );
return false;
}
}
return true; //Always return true for master connections.
} );
$db_connection_obj->getADODbObject()->SetFetchMode( ADODB_FETCH_ASSOC );
$db_connection_obj->getADODbObject()->noBlobs = true; //Optimization to tell ADODB to not bother checking for blobs in any result set.
$db_connection_obj->getADODbObject()->fmtTimeStamp = "'Y-m-d H:i:s'";
if ( Debug::getVerbosity() >= 12 ) {
//Use 1 instead of TRUE, so it only outputs some debugging and not things like backtraces for every cache read/write.
//Set to 99 to get all debug output.
$db_connection_obj->getADODbObject()->debug = 1;
}
if ( isset( $config_vars['database']['disable_row_count'] ) && $config_vars['database']['disable_row_count'] == true ) {
//Dont count rows for pagination, much faster. However two queries must be run to tell if we are at the last page or not.
$db_connection_obj->getADODbObject()->pageExecuteCountRows = false;
}
$db->addConnection( $db_connection_obj );
}
unset( $db_hosts, $db_host_arr, $db_connection_obj );
$db->setSessionInitSQL( 'SET datestyle = \'ISO\'' ); //Needed for ADODB to properly parse dates, as we removed it from ADODB as an optimization so it can be delayed until the first query is executed.
//$db->setSessionInitSQL( 'SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL REPEATABLE READ' ); //This is required to properly handle simultaneous recalculations of timesheets/pay stubs. We moved this to trigger via setTransactionMode() only for certain operations instead though.
} else {
//To enable PDO support. Type: pdo_pgsql
//$dsn = $config_vars['database']['type'].'://'.$config_vars['database']['user'].':'.$config_vars['database']['password'].'@'.$config_vars['database']['host'].'/'.$config_vars['database']['database_name'].'?persist';
//$db = ADONewConnection( $dsn );
$db = ADONewConnection( $config_vars['database']['type'] );
/** @noinspection PhpUndefinedConstantInspection */
$db->SetFetchMode( ADODB_FETCH_ASSOC );
if ( isset( $config_vars['database']['persistent_connections'] ) && $config_vars['database']['persistent_connections'] == true ) {
$db->PConnect( $config_vars['database']['host'], $config_vars['database']['user'], $config_vars['database']['password'], $config_vars['database']['database_name'] );
} else {
$db->Connect( $config_vars['database']['host'], $config_vars['database']['user'], $config_vars['database']['password'], $config_vars['database']['database_name'] );
}
$db->noBlobs = true; //Optimization to tell ADODB to not bother checking for blobs in any result set.
//Use long timezone format because PostgreSQL 8.1 doesn't support some short names, like SGT,IST
//Using "e" for the timezone fixes the Asia/Calcutta & IST bug where the two were getting confused.
//We set the session timezone in PostgreSQL, so 'e' shouldn't be required anymore.
//$db->fmtTimeStamp = "'Y-m-d H:i:s e'";
$db->fmtTimeStamp = "'Y-m-d H:i:s'";
if ( Debug::getVerbosity() >= 12 ) {
//Use 1 instead of TRUE, so it only outputs some debugging and not things like backtraces for every cache read/write.
//Set to 99 to get all debug output.
$db->debug = 1;
}
$db->Execute( 'SET datestyle = \'ISO\'' ); //Needed for ADODB to properly parse dates, as we removed it from ADODB as an optimization so it can be delayed until the first query is executed.
//$db->Execute( 'SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL REPEATABLE READ' ); //This is required to properly handle simultaneous recalculations of timesheets/pay stubs. We moved this to trigger via setTransactionMode() only for certain operations instead though.
if ( isset( $config_vars['database']['disable_row_count'] ) && $config_vars['database']['disable_row_count'] == true ) {
//Dont count rows for pagination, much faster. However two queries must be run to tell if we are at the last page or not.
$db->pageExecuteCountRows = false;
}
}
} catch ( Exception $e ) {
Debug::Text( 'Error connecting to the database!', __FILE__, __LINE__, __METHOD__, 1 );
throw new DBError( $e );
}
if ( ( isset( $config_vars['other']['installer_enabled'] ) && $config_vars['other']['installer_enabled'] == true ) || ( isset( $_SERVER['SCRIPT_FILENAME'] ) && stripos( $_SERVER['SCRIPT_FILENAME'], 'unattended_upgrade' ) !== false ) ) { //Make sure we always check the schema versions when run from unattended_upgrade, just in case the upgrade failed and we need to determine if we are in UUID mode or not.
//Make sure that during initial installation we confirm that the database/table actually exists, otherwise this can throw a fatal SQL error.
$install_obj = new Install();
$install_obj->setDatabaseConnection( $db ); //Default connection
if ( $install_obj->checkSystemSettingTableExists() == true ) {
//Check to see if the DB schema is before the UUID upgrade (schema 1070 or older) and set the $PRIMARY_KEY_IS_UUID accordingly.
// THIS IS in tools/unattended_install.php, tools/unattended_upgrade.php, includes/database.inc.php as well.
if ( (int)SystemSettingFactory::getSystemSettingValueByKey( 'schema_version_group_A' ) < 1100 ) {
Debug::Text( 'Setting PRIMARY_KEY_IS_UUID to FALSE due to pre-UUID schema version: ' . SystemSettingFactory::getSystemSettingValueByKey( 'schema_version_group_A' ), __FILE__, __LINE__, __METHOD__, 1 );
$PRIMARY_KEY_IS_UUID = false;
}
}
}
} else {
Debug::Text( 'Database config options are not set... Unable to connect to database.', __FILE__, __LINE__, __METHOD__, 1 );
throw new DBError( new Exception );
}
}
//Set timezone to system local timezone by default. This is so we sync up all timezones in the database and PHP.
//This fixes timezone bugs mainly in maintenance scripts. We used to default this to just GMT, but that can cause additional problems in threaded environments.
//This must be run AFTER the database connection has been made to work properly.
if ( !isset( $config_vars['other']['system_timezone'] ) || ( isset( $config_vars['other']['system_timezone'] ) && $config_vars['other']['system_timezone'] == '' ) ) {
$config_vars['other']['system_timezone'] = @date( 'e' );
}
TTDate::setTimeZone( $config_vars['other']['system_timezone'], false, false ); //Don't force SQL to be executed here, as an optimization to avoid DB connections when calling things like getProgressBar()
?>