*/ namespace Bitweaver; /** * required setup */ define( 'STORAGE_BINARY', 1 ); define( 'STORAGE_IMAGE', 2 ); interface BitCacheable { public function getCacheKey(); } /** * Abstract root class for all DB-aware bitweaver objects. * * Every class that touches the database — content types, user objects, system * singletons — extends BitBase directly or via LibertyBase / LibertyContent. * It provides four concerns shared by the entire class hierarchy: * * ## Database connection * * `$mDb` is set from the global `$gBitDb` on construct and on `__wakeup()`. * All DB calls go through this reference. `StartTrans()` / `CompleteTrans()` / * `RollbackTrans()` are thin wrappers that also clear the APCu object cache on * transaction start (data change implied). * * ## Data hash * * `$mInfo` is the primary data store for a loaded object — an associative array * of DB column values populated by `load()` and read via `getField()`. * `$mErrors` accumulates validation failures from `verify()` and is checked by * callers via `count($this->mErrors) == 0`. * * ## Object caching (APCu) * * Optional APCu caching is gated by `isCacheActive()`. Cache keys are scoped * per host + DB + class + content_id via `getCacheUuidFromKey()`. Subclasses * opt in by overriding `isCacheableClass()` (returns true) and `getCacheKey()` * (returns the primary key value). Cache is cleared on `StartTrans()` and on * `__destruct()` if the object is marked non-cacheable. * * ## List utilities * * `prepGetList(&$pListHash)` normalises pagination parameters (offset, max_records, * sort_mode, find) before any `getList()` query. `postGetList(&$pListHash)` builds * the `listInfo` hash that Smarty pagination helpers consume. Both are static so * they can be called without a loaded object instance. * * @package kernel */ abstract class BitBase { /** * Error hash that will contain an error codes we encounter along * the way this hash can be used by presentation layer ti give feedback * to the user. * @todo not used yet * @private */ public $mErrors = []; /** * Same idea as the error hash but this is for successful operations * @private */ public $mSuccess; /** * String used to refer to preference caching and database table * @private */ public $mName; /** * Used to store database mechanism * @private */ public $mDb; /** * Used to store database type * @private */ public $dType; /** * Was the object created from object caching mechanism like apcu * @private */ public $mCacheObject; /** * Standard Query Cache Time. Variable can be set to 0 to flush particular queries * @private */ public $mCacheTime; /** * Data hash that represents this classes row(s) in the db **/ public $mInfo = []; /** * Data hash that contains logging information relevant to database operations **/ public $mLogs = []; /** * Last time object was cached **/ public $mLastOutputTime; /** * Debug timing **/ public $mDebugMicrotime; protected $mPreventCache = true; public const int CACHE_STATE_NONE = 0; public const int CACHE_STATE_DELETE = -1; public const int CACHE_STATE_ADDED = 1; public const int CACHE_STATE_STORED = 2; public function __construct( $pName = '' ) { global $gBitDb; $this->mName = $pName; // On construct we will set the cache time to none to force a reload of everything for this singleton $this->mCacheTime = BIT_QUERY_CACHE_DISABLE; if( is_object( $gBitDb ) ) { $this->setDatabase($gBitDb); } $this->mErrors = []; $this->mInfo = []; } public function __destruct() { unset( $this->mDb ); $this->storeInCache(); } public function __sleep() { unset( $this->mDb ); // On sleep we will set the cache time to default $this->mCacheTime = self::cacheQueryDefaultTime(); $this->mCacheObject = true; return [ 'mCacheTime', 'mCacheObject' ]; } public function __wakeup() { global $gBitDb; $this->setDatabase( $gBitDb ); $this->mErrors = []; } protected function load() { // load implies to purge all existing cache data $this->clearFromCache(); return true; } /** * During initialisation, we assign a name which is used by the class. * @param string pName a unique identified used in caching and database * mechanisms **/ function BitBase( $pName = '' ) { self::__construct( $pName ); } /** * Delete content object and all related records * * @return bool */ protected function expunge() { $this->clearFromCache(); return true; } protected function cacheQueryTime() { return $this->mCacheTime; } public static function cacheQueryDefaultTime() { return self::isRefreshRequest() ? BIT_QUERY_CACHE_TIME : 0; } public function clearFromCache( &$pParamHash=null ) { global $gBitSystem; $this->mCacheTime = BIT_QUERY_CACHE_DISABLE; $this->mPreventCache = true; if( $gBitSystem && static::isCacheActive() && ($cacheKey = $this->getCacheUuid()) ) { // if there are multiple instances of this object, there is a race condition since it's possible cleared cached objects remain by getting re-storeInCache because clear was not called on that instance. urgh. // BitSystem keeps a master list of keys to purge on page destruction just in case this happens $gBitSystem->queueClearFromCache( $cacheKey ); $ret = apcu_delete( $cacheKey ); //var_dump( 'DELETE '.get_called_class().' '.$cacheKey ); } } /** * storeInCache * * @param string // $pCacheKey unique identifier for object in cache store * @access public * @return bool true on success, false on failure */ protected function storeInCache() { $ret = false; if( static::isCacheActive() && ($cacheKey = $this->getCacheUuid()) ) { global $gBitSystem; ( $this->isCacheableObject() && $gBitSystem && !$gBitSystem->isPurgedFromCache( $cacheKey ) ) ? ( $this->mCacheObject ? $ret = apcu_store( $cacheKey, $this, 3600 ) : $ret = apcu_add( $cacheKey, $this, 3600 ) ) : $this->clearFromCache(); } return $ret; } public static function loadFromCache( $pCacheKey, $pContentTypeGuid = null ) { $ret = null; //vd( 'LOAD '.get_called_class().' '.static::getCacheUuidFromKey( $pCacheKey, $pContentTypeGuid ) ); if( static::isCacheActive() && static::isCacheableClass() && !empty( $pCacheKey ) ) { if( $ret = apcu_fetch( static::getCacheUuidFromKey( $pCacheKey, $pContentTypeGuid ) ) ) { $ret->mCacheObject = true; //vd( 'LOAD SUCCESS '.get_class( $ret ).' ' .$ret->getField( 'content_id' ) ); } //vd( 'LOAD FAILED' ); } return $ret; } static public function getObjectById( $pObjectId ) { $className = get_called_class(); if( !($ret = $className::loadFromCache( $pObjectId ) ) ) { $ret = new $className( $pObjectId ); $ret->load(); } return $ret; } public static function getCacheClass() { return static::getClass(); } public function getCacheUuid() { return static::getCacheUuidFromKey( $this->getCacheKey() ); } public function getCacheKey() { // default returns no key return null; } public static function getCacheUuidFromKey( $pCacheUuid = '', $pContentTypeGuid = null ) { global $gBitDbName, $gBitDbHost; $ret = BitBase::getParameter( $_SERVER, 'HTTP_HOST', 'unknown' ).':'.$gBitDbName.'@'.$gBitDbHost.':'.($pContentTypeGuid ? $pContentTypeGuid : static::getCacheClass()).'#'.$pCacheUuid; return $ret; } public static function isCacheActive() { // only apc is supported for now. return function_exists( 'apcu_add' ) && defined( 'BIT_CACHE_OBJECTS' ) && BIT_CACHE_OBJECTS && !self::isRefreshRequest(); } public function isCacheableObject() { return method_exists( $this, 'getCacheKey' ) && empty( $this->mPreventCache ); } public static function isCacheableClass() { return false; } public function isCached() { return apcu_exists( $this->getCacheUuid() ); } public function setCacheableObject( $pCacheable = true ) { $this->mPreventCache = empty( $pCacheable ); } public static function isRefreshRequest() { static $isRefresh = null; if( $isRefresh === null ) { $isRefresh = isset($_SERVER['HTTP_CACHE_CONTROL']) &&(/*$_SERVER['HTTP_CACHE_CONTROL'] === 'max-age=0' || */ $_SERVER['HTTP_CACHE_CONTROL'] == 'no-cache'); } return $isRefresh; } final public static function getClass() { return get_called_class(); } public function getConfig( string $pName, string $pDefault = '' ) { global $gBitSystem; return $gBitSystem->getConfig( $pName, $pDefault ); } public function isFeatureActive( $pFeatureName ) { global $gBitSystem; return $gBitSystem->isFeatureActive( $pFeatureName ); } /** * Sets database mechanism for the instance * @param object pDB the instance of the database mechanism **/ public function setDatabase( &$pDB ) { // set internal db and retrieve values $this->mDb = &$pDB; $this->dType = $this->mDb->mType; } /** * Determines if there is a valide database connection **/ public function isDatabaseValid(): bool { return !empty( $this->mDb ) && $this->mDb->isValid(); } /** * Return pointer to current Database **/ public function getDb() { return !empty( $this->mDb ) ? $this->mDb : null; } /** * Begin transaction in database. Make sure to handle object caching here **/ function StartTrans() { // Database transaction locking implies inherent data change. clear object in cache. $this->clearFromCache(); $this->mDb->StartTrans(); } /** * Finish transaction in database. **/ function CompleteTrans() { $this->mDb->CompleteTrans(); } /** * Rollback transaction in database. **/ function RollbackTrans() { $this->mDb->RollbackTrans(); } function debugMarkTime() { $this->mDebugMicrotime = microtime(1); } function debugOutput( $pString ) { global $gDebug, $gArgs, $argv; if( $gDebug || !defined( 'IS_LIVE' ) || !IS_LIVE || !empty( $gArgs['log'] ) ) { $elapsed = empty( $this->mLastOutputTime ) ? (float)0.000 : microtime(1) - (float)$this->mLastOutputTime; if( !empty( $this->mDebugMicrotime ) ) { $pString = "ELAPSED TIME: ".round( microtime(1) - $this->mDebugMicrotime, 3).' sec, +'.round( $elapsed, 3 ).' '.$pString; } bit_error_log( $pString ); $this->mLastOutputTime = microtime(1); } } // =-=-=-=-=-=-=-=-=-=-=- Non-DB related functions =-=-=-=-=-=-=-=-=-=-=-=-= /** * verifyIdParamter Determines if any given variable exists and is a number * * @param mixed $pKey this can be a string, number or array. if it's an array, all values in the array will be checked to see if they are numeric * @access public * @return bool true if the input was numeric, false if it wasn't */ public static function verifyIdParameter( &$pParamHash, $pKey ) { // check all possibilities as quickly as possible as this function is called frequently return !empty( $pParamHash[$pKey] ) && BitBase::verifyId( $pParamHash[$pKey] ); } /** * verifyId Determines if any given variable exists and is a number * * @param mixed $pId this can be a string, number or array. if it's an array, all values in the array will be checked to see if they are numeric * @access public * @return bool true if the input was numeric, false if it wasn't */ public static function verifyId( mixed $pId ): bool { if( empty( $pId ) || ( is_int( $pId ) && $pId < -1 ) ) { return false; } if( is_array( $pId )) { foreach( $pId as $id ) { if( (is_int( $id ) || ctype_digit( (string) $id ) || (is_numeric( $id ) ? (int) $id == $id : false)) ) { return false; } } return true; } return !empty( $pId ) && (is_int( $pId ) || ctype_digit( $pId ) || is_numeric( $pId )) && ($pId < 0x7FFFFFFF) && ((int) $pId == $pId); } /** * getParameter Gets a hash value it exists, or returns an optional default * * @param array $pParamHash Hash of key=>value pairs * @param string $pKey Key used to search for value * @param string $pDefaultValue Default value to return if not found. null if nothing is passed in. * @return array|string|null */ public static function getParameter( array $pParamHash, string $pKey, ?string $pDefaultValue = null ): array|string|null { return $pParamHash[$pKey] ?? $pDefaultValue; } /** * getIdParameter Gets an in-bounds, integer hash value it exists, or returns an optional default * * @param array $pParamHash Hash of key=>value pairs * @param string $pKey Key used to search for value * @param string $pDefaultValue Default value to return if not found. null if nothing is passed in. * @return bool true if the input was numeric, false if it wasn't */ public static function getIdParameter( array &$pParamHash, string $pKey, ?string $pDefaultValue = null ) { return BitBase::verifyIdParameter( $pParamHash, $pKey ) ? $pParamHash[$pKey] : $pDefaultValue; } /** * This method should be THE method used to display a template. php files should not * access $gBitSmarty directly. * * @param string pMsg error message to be displayed * @return void this function will DIE DIE DIE!!! * @access public **/ public function display( $pPackage, $pTemplate ) { global $gBitSmarty, $gBitLanguage, $style, $style_base; if( !empty( $style ) && !empty( $style_base )) { $_smarty_tpl_file = file_exists(THEMES_PATH."styles/$style_base/$pTemplate") ? 'file:'.BIT_STYLES_PATH."/$style_base/$pTemplate" : 'file:'.BIT_ROOT_PATH."$pPackage/templates/$pTemplate"; } /* global $gBitLanguage, $style, $style_base; if (isset($style) && isset($style_base)) { if (file_exists(BIT_STYLES_PATH."/$style_base/$_smarty_tpl_file")) { $_smarty_tpl_file = BIT_STYLES_PATH."/$style_base/$_smarty_tpl_file"; } } */ $gBitSmarty->display( $_smarty_tpl_file ); // $gBitSmarty->display( 'bitpackage:'.$pPackage.$pTemplate ); } /** * Assign an entry to the mInfo hash if the current object is valid * @param string pFieldName the hash key to retrieve the value * @param string pValue the value of the hash key * @return bool false if not valid setting **/ public function setField( string $pFieldName, string $pValue ): bool { $ret = false; if( $this->mDb->isValid() ) { $this->mInfo[$pFieldName] = $pValue; $ret = true; } return $ret; } /** * Returns entry from the mInfo hash if field exists * @param string pFieldName the hash key to retrieve the value * @param string pDefault the value to return of there is now hash value present **/ public function getField( string $pFieldName, ?string $pDefault = null ) { return !empty( $this->mInfo[$pFieldName] ) ? $this->mInfo[$pFieldName] : $pDefault; } /** * Prepares parameters with default values for any getList function * @param array pListHash hash of parameters for any getList() function * @return void the link to display the page. */ public static function prepGetList( array &$pListHash ): void { global $gBitSmarty, $gBitSystem; $pListHash['page'] = !empty( $pListHash['page'] ) ? $pListHash['page'] : 1; // valid_sort_modes are set, we check them against our selected sort_mode if( !empty( $pListHash['sort_mode'] ) && !empty( $pListHash['valid_sort_modes'] ) && is_array( $pListHash['valid_sort_modes'] )) { if( is_string( $pListHash['sort_mode'] )) { if( !static::verifySortMode( $pListHash['sort_mode'], $pListHash['valid_sort_modes'] )) { $pListHash['sort_mode'] = ''; } } elseif( is_array( $pListHash['sort_mode'] )) { // make sure all values of the sort_mode array match something in the valid valid_sort_modes hash foreach( $pListHash['sort_mode'] as $key => $mode ) { if( !static::verifySortMode( $mode, $pListHash['valid_sort_modes'] )) { unset( $pListHash['sort_mode'][$key] ); } } } } if( empty( $pListHash['max_records'] ) || !is_numeric( $pListHash['max_records'] ) ) { global $gBitSystem; $pListHash['max_records'] = $gBitSystem->getConfig( "max_records", 10 ); } $pListHash['max_records'] = (int)$pListHash['max_records']; if( !isset( $pListHash['offset'] ) || !is_numeric( $pListHash['offset'] ) ) { $pListHash['offset'] = 0; if( static::verifyId( $pListHash['page'] ) ) { $pListHash['offset'] = ($pListHash['page'] - 1) * $pListHash['max_records']; } else { if( !empty( $_REQUEST["offset"] )) { $pListHash['offset'] = $_REQUEST['offset']; } elseif( isset( $_REQUEST['page'] ) && is_numeric( $_REQUEST['page'] ) && $_REQUEST['page'] > 0 ) { $pListHash['offset'] = ($_REQUEST['page'] ?? 1 - 1) * $pListHash['max_records']; } elseif( isset( $_REQUEST['list_page'] ) && is_numeric( $_REQUEST['list_page'] ) && $_REQUEST['list_page'] > 0 ) { $pListHash['offset'] = ( $_REQUEST['list_page'] - 1 ) * $pListHash['max_records']; } } } // migrate towards a safer hash key if( empty( $pListHash['user_id'] ) && !empty( $pListHash['lookup_user_id'] ) ) { $pListHash['user_id'] = $pListHash['lookup_user_id']; } // Don't use $_REQUEST["find"] as it can really screw with modules on search pages $pListHash['find'] = !empty( $pListHash["find"] ) ? $pListHash["find"] : null; $gBitSmarty->assign( 'find', $pListHash['find'] ); $pListHash['date'] = $_REQUEST['date'] ?? $gBitSystem->getUTCTime(); if( empty( $pListHash['load_comments'] )) { $pListHash['load_comments'] = false; } if( empty( $pListHash['load_num_comments'] )) { $pListHash['load_num_comments'] = false; } if( empty( $pListHash['parse_data'] )) { $pListHash['parse_data'] = false; } } /** * verifySortMode is used to validate a given sort_mode agains an array of valid sort modes * * @param string $pSortMode sort mode to check * @param array $pValidSortModes array of available sort modes * @access public * @return bool true on success, false on failure */ public static function verifySortMode( $pSortMode, $pValidSortModes ) { if( !empty( $pSortMode ) && is_string( $pSortMode ) && !empty( $pValidSortModes ) && is_array( $pValidSortModes )) { foreach( $pValidSortModes as $mode ) { // we will not check the table - that would just be too complicated... if( preg_match( "/^(\w+\.)?{$mode}_(desc|asc)$/", $pSortMode )) { return true; } } } return false; } /** * Updates results from any getList function to provide the control set * displaying in the smarty template * @param array hash of parameters returned by any getList() function * @return - none the hash is updated via the reference */ public static function postGetList( array &$pListHash ): void { global $gBitSystem; $pListHash['listInfo']['page_records'] = !empty( $pListHash['page_records']) ? $pListHash['page_records'] : $pListHash['max_records']; if( !isset( $pListHash['cant'] ) ) { $pListHash['cant'] = $pListHash['max_records']; } if( !isset( $pListHash['offset'] ) || !is_numeric( $pListHash['offset'] ) ) { $pListHash['offset'] = 0; } $pListHash['listInfo']['total_records'] = $pListHash['cant']; $pListHash['listInfo']['total_pages'] = ceil( $pListHash['cant'] / $pListHash['max_records'] ); $pListHash['listInfo']['current_page'] = 1 + $pListHash['offset'] / $pListHash['max_records']; $pListHash['listInfo']['page'] = $pListHash['page'] ?? 1; isset( $pListHash["cant"] ) && $pListHash["cant"] > $pListHash['offset'] + $pListHash['max_records'] ? $pListHash['listInfo']['next_offset'] = $pListHash['offset'] + $pListHash['max_records'] : $pListHash['listInfo']['next_offset'] = -1; // If offset is > 0 then prev_offset $pListHash['listInfo']['prev_offset'] = ($pListHash['offset'] > 0) ? $pListHash['offset'] - $pListHash['max_records'] : -1; $pListHash['listInfo']['offset'] = $pListHash['offset']; $pListHash['listInfo']['find'] = $pListHash['find']; if( !empty( $pListHash['sort_mode'] ) ) { $pListHash['listInfo']['sort_mode'] = $pListHash['sort_mode']; } $pListHash['listInfo']['max_records'] = $pListHash['max_records']; $pListHash['listInfo']['block_pages'] = 3; $pListHash['listInfo']['start_block'] = floor( $pListHash['offset'] / $pListHash['max_records'] ) * $pListHash['max_records'] + 1; // calculate what links to show if( $gBitSystem->isFeatureActive( 'site_direct_pagination' ) ) { // number of continuous links to display on either side $continuous = 5; // number of skipping links to display on either side $skipping = 5; // size of steps to take when skipping // if you have more than 1000 pages, you should consider not using the pagination form if( $pListHash['listInfo']['total_pages'] < 50 ) { $step = 5; } elseif( $pListHash['listInfo']['total_pages'] < 100 ) { $step = 10; } elseif( $pListHash['listInfo']['total_pages'] < 250 ) { $step = 25; } elseif( $pListHash['listInfo']['total_pages'] < 500 ) { $step = 50; } else { $step = 100; } $prev = ( $pListHash['listInfo']['current_page'] - $continuous > 0 ) ? $pListHash['listInfo']['current_page'] - $continuous : 1; $next = ( $pListHash['listInfo']['current_page'] + $continuous < $pListHash['listInfo']['total_pages'] ) ? $pListHash['listInfo']['current_page'] + $continuous : $pListHash['listInfo']['total_pages']; for( $i = $pListHash['listInfo']['current_page'] - 1; $i >= $prev; $i -= 1 ) { $pListHash['listInfo']['block']['prev'][$i] = $i; } if( $prev != 1 ) { // replace the last of the continuous links with a ... $pListHash['listInfo']['block']['prev'][$i + 1] = "…"; // add $skipping links to pages separated by $step pages if( ( $min = $pListHash['listInfo']['current_page'] - $continuous - $step * $skipping ) < 0 ) { $min = 0; } for( $j = floor( $i / $step ) * $step; $j > $min; $j -= $step ) { $pListHash['listInfo']['block']['prev'][$j] = $j; } $pListHash['listInfo']['block']['prev'][1] = 1; } // reverse array that links are in the correct order if( !empty( $pListHash['listInfo']['block']['prev'] ) ) { $pListHash['listInfo']['block']['prev'] = array_reverse( $pListHash['listInfo']['block']['prev'], true ); } // here we start adding next links for( $i = $pListHash['listInfo']['current_page'] + 1; $i <= $next; $i += 1 ) { $pListHash['listInfo']['block']['next'][$i] = $i; } if( $next != $pListHash['listInfo']['total_pages'] ) { // replace the last of the continuous links with a ... $pListHash['listInfo']['block']['next'][$i - 1] = "…"; // add $skipping links to pages separated by $step pages if( ( $max = $pListHash['listInfo']['current_page'] + $continuous + $step * $skipping ) > $pListHash['listInfo']['total_pages'] ) { $max = $pListHash['listInfo']['total_pages']; } for( $j = ceil( $i / $step ) * $step; $j < $max; $j += $step ) { $pListHash['listInfo']['block']['next'][$j] = $j; } $pListHash['listInfo']['block']['next'][$pListHash['listInfo']['total_pages']] = $pListHash['listInfo']['total_pages']; } } } }