*/ // +----------------------------------------------------------------------+ // | Copyright (c) 2004, bitweaver.org // +----------------------------------------------------------------------+ // | All Rights Reserved. See below for details and a complete list of authors. // | Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See http://www.gnu.org/copyleft/lesser.html for details // | // | For comments, please use phpdocu.sourceforge.net documentation standards!!! // | -> see http://phpdocu.sourceforge.net/ // +----------------------------------------------------------------------+ // | Authors: spider // +----------------------------------------------------------------------+ /** * required setup */ namespace Bitweaver\Liberty; use Bitweaver\BitBase; use Bitweaver\BitCacheable; use Bitweaver\BitSystem; use Bitweaver\Users\RoleUser; use Bitweaver\KernelTools; /** * Maximum lengths for database fields */ if( !defined( 'BIT_CONTENT_MAX_TITLE_LEN' ) ) { define( 'BIT_CONTENT_MAX_TITLE_LEN', 160); } define( 'BIT_CONTENT_MAX_LANGUAGE_LEN', 4); define( 'BIT_CONTENT_MAX_IP_LEN', 39); define( 'BIT_CONTENT_MAX_FORMAT_GUID_LEN', 16); if( !defined( 'BIT_CONTENT_DEFAULT_STATUS' ) ) { define( 'BIT_CONTENT_DEFAULT_STATUS', 50); } //$gBitSystem->getConfig( 'liberty_status_deleted', -999 ) ); //$gBitSystem->getConfig( 'liberty_status_threshold_private', -40 ) ); //$gBitSystem->getConfig( 'liberty_status_threshold_protected', -20 ) ); //$gBitSystem->getConfig( 'liberty_status_threshold_hidden', -10 ) ); define( 'LIBERTY_SPLIT_REGEX', "!\.[3]split\.[3][\t ]*\n?!" ); /** * Base class for all content types managed by the liberty content engine. * * Every piece of user-visible content in bitweaver — wiki pages, articles, blog posts, * contacts, stock items, movements — is backed by a row in liberty_content and * represented by a subclass of LibertyContent. The shared row carries fields common * to all content: title, creator/modifier user_id, timestamps, format_guid, * content_status_id, and the raw data blob. * * ## Lifecycle * * Subclasses follow this pattern: * - **`verify(&$pParamHash)`** — validate and normalise inbound data; build * `$pParamHash['content_store']` ready for DB write. Call `parent::verify()` * first, then extend with package-specific checks. * - **`store(&$pParamHash)`** — write to liberty_content (insert or update), then * call `parent::store()`. Opens a transaction; the subclass writes its own table * inside the same transaction. * - **`load()`** — load from DB into `$this->mInfo`. The subclass loads its own * table joined to liberty_content, then calls `parent::load()` to trigger * services and load preferences. * - **`expunge()`** — delete the content item and all related records. The * subclass deletes its own table rows first, then calls `parent::expunge()` which * removes xref rows, history, hits, aliases, and the liberty_content row itself. * * ## Permissions * * Five permission strings are set as class properties and defaulted in `__construct()`: * - `mViewContentPerm` — if empty, view is always allowed * - `mCreateContentPerm` — required to create new content of this type * - `mUpdateContentPerm` — required to edit existing content * - `mExpungeContentPerm` — required to delete content * - `mAdminContentPerm` — supersedes all other checks; grants full access * * Per-item permission overrides (liberty_content_permissions) can supplement or * revoke role permissions for a specific content item independently of global roles. * * ## XRef System * * LibertyContent is the gateway into the xref system for content subclasses: * - `loadXrefInfo()` — populate `$this->mXrefInfo` (a LibertyXrefContent) with all * display groups and their rows for this content item. Call this from page files * before assigning `gXrefInfo` to Smarty. Package classes override this to * enrich rows (e.g. resolving contact titles from xref content_ids). * - `loadXrefTypeList()` — load sort_order=0 type markers into mInfo. * - `storeXref(&$pParamHash)` / `stepXref(&$pParamHash)` — write xref rows. * - `getXrefListTemplate()` / `getXrefRecordTemplate()` / `getXrefEditTemplate()` — * resolve the correct Smarty template for a group or item. * * ## Content Status * * `liberty_content.content_status_id` is a numeric threshold: * - >= 50 public / available * - < 0 various hidden/private/deleted tiers (thresholds configurable via kernel_config) * * ## Services * * `invokeServices($pServiceFunction, &$param)` calls all registered service functions * of a given type (e.g. `content_store_function`, `content_list_sql_function`). * Services augment content behaviour — categorisation, search indexing, access * control — without modifying this class. * * @package liberty */ class LibertyContent extends LibertyBase implements BitCacheable { /** * Content Id if an object has been loaded * @public */ public $mContentId; /** * If this content is being viewed within a structure * @public */ public $mStructureId; /** * Content type GUID for this LibertyContent object * @public */ public $mContentTypeGuid; /** Set when handler_package differs from content_type_guid (e.g. 'stock' for stockcomponent/stockassembly). Used to scope xref_item lookups via IN(). */ public string $mPackageGuid = ''; /** Cached LibertyXrefType instance scoped to mContentTypeGuid + mPackageGuid. Lazy-initialised by xrefType(); reset to null in registerContentType() so a re-registration gets a fresh instance. Not serialised — rebuilt on first use after deserialise. */ protected ?LibertyXrefType $mXrefType = null; /** * mInfo key used by loadXrefTypeList(); subclasses may override (e.g. 'contact_types') */ protected $mXrefTypeKey = 'xref_types'; /** * Content type hash for this LibertyContent object * @public */ public $mType; /** Populated by loadXrefInfo() — LibertyXrefContent instance for this content item */ public ?LibertyXrefContent $mXrefInfo = null; /** *Permissions hash specific to the user accessing this LibertyContent object * @public */ public $mUserContentPerms; /** * Preferences hash specific to this LibertyContent object - accessed via getPreference/storePreference * @private */ public $mPrefs = null; /** * Control permission specific to this LibertyContent type * @private */ public $mViewContentPerm; public $mUpdateContentPerm; public $mCreateContentPerm; public $mExpungeContentPerm; public $mAdminContentPerm; /** * Construct an empty LibertyBase object with a blank permissions array */ public function __construct() { parent::__construct(); $this->mPrefs = null; // init to null so getPreference can determine if a load is necessary // NOTE: we are not assigning anything to mViewContentPerm. if this is empty, we will return true in hasViewPermission() if( empty( $this->mUpdateContentPerm )) { $this->mUpdateContentPerm = 'p_admin_content'; } if( empty( $this->mCreateContentPerm )) { $this->mCreateContentPerm = 'p_admin_content'; } if( empty( $this->mExpungeContentPerm )) { $this->mExpungeContentPerm = 'p_admin_content'; } if( empty( $this->mAdminContentPerm )) { $this->mAdminContentPerm = 'p_admin_content'; } } public static function isCacheableClass() { global $gBitSystem; // new feature, cache by default in development systems only return !$gBitSystem->isLive(); } public function isCacheableObject() { return parent::isCacheableObject() && !empty( $this->mContentId ); } public function getCacheKey() { if( $this->isValid() ) { return $this->mContentId; } } public function __sleep() { return array_merge( parent::__sleep(), [ 'mContentId', 'mInfo', 'mStructureId', 'mContentTypeGuid', 'mType', 'mUserContentPerms', 'mPrefs', 'mViewContentPerm', 'mUpdateContentPerm', 'mCreateContentPerm', 'mExpungeContentPerm', 'mAdminContentPerm' ] ); } /** * load Assume a derived class has joined on the liberty_content table, and loaded it's columns already. * * @return void */ public function load() { if( !empty( $this->mInfo['content_type_guid'] )) { global $gLibertySystem, $gBitSystem, $gBitUser; $this->loadPreferences(); $this->mInfo['content_type'] = $gLibertySystem->mContentTypes[$this->mInfo['content_type_guid']]; $this->invokeServices( 'content_load_function', $this ); } } /** * Verify the core class data required to update the liberty_content table entries * * Verify will build an array [content_store] with all of the required values * and populate it with the relevent data to create/update the liberty_content * table record * * @param array $pParamHash Array of content data to be stored * * @param array $pParamHash[content_id] * @param array $pParamHash[user_id] * @param array $pParamHash[modifier_user_id] * @param array $pParamHash[created] * @param array $pParamHash[last_modified] * @param array $pParamHash[content_type_guid] * @param array $pParamHash[format_guid] * @param array $pParamHash[last_hit] * @param array $pParamHash[event_time] * @param array $pParamHash[hits] * @param array $pParamHash[lang_code] * @param array $pParamHash[title] * @param array $pParamHash[ip] * @param array $pParamHash[edit] * @access public * @return bool true on success, false on failure - mErrors will contain reason for failure */ public function verify( array &$pParamHash ): bool { global $gLibertySystem, $gBitSystem, $gBitLanguage, $gBitUser; // It is possible a derived class set this to something different if( empty( $pParamHash['content_type_guid'] ) ) { $pParamHash['content_type_guid'] = $this->mContentTypeGuid; } if( empty( $pParamHash['user_id'] ) ) { $pParamHash['user_id'] = $gBitUser->getUserId(); } if( $this->verifyIdParameter( $pParamHash, 'content_id' ) && $pParamHash['content_id'] != $this->mContentId ) { // we have request for a content, but is not the same as this object, something stinky going on. // Unset the pParamHash['content_id'] and let mContentId be used going forward unset( $pParamHash['content_id'] ); } if( !$this->verifyIdParameter( $pParamHash, 'content_id' ) ) { if( !$this->verifyId( $this->mContentId ) ) { // These should never be updated, only inserted $pParamHash['content_store']['created'] = !empty( $pParamHash['created'] ) ? $pParamHash['created'] : $gBitSystem->getUTCTime(); // This may get overridden by owner set $pParamHash['content_store']['user_id'] = $pParamHash['user_id']; // Set a default status when creating if none is set // This may get overwritten below if( empty($pParamHash['content_store']['content_status_id'] ) ){ $pParamHash['content_store']['content_status_id'] = $gBitSystem->getConfig('liberty_default_status', BIT_CONTENT_DEFAULT_STATUS); } } else { $pParamHash['content_id'] = $this->mContentId; } } if( BitBase::verifyIdParameter( $pParamHash, 'content_id' ) ) { $pParamHash['content_store']['content_id'] = $pParamHash['content_id']; } // Are we allowed to override owner? if( !empty($pParamHash['owner_id'] ) ) { if( $gBitUser->isAdmin() || ($gBitSystem->isFeatureActive('liberty_allow_change_owner') && $gBitUser->hasPermission('p_liberty_edit_content_owner') && !empty($pParamHash['owner_id']) && !empty($pParamHash['current_owner_id']) && $pParamHash['owner_id'] != $pParamHash['current_owner_id']) ) { // If an owner is being set override user_id $pParamHash['content_store']['user_id'] = $pParamHash['owner_id']; } } // Do we need to change the status if (!empty($pParamHash['content_status_id'])) { if( $this->hasUserPermission( 'p_liberty_edit_content_status' ) || $gBitUser->hasUserPermission( 'p_liberty_edit_all_status') ) { $allStatus = $this->getAvailableContentStatuses(); if (empty($allStatus[$pParamHash['content_status_id']])) { $this->mErrors['content_status_id'] = "No such status ID or permission denied."; } else { $pParamHash['content_store']['content_status_id'] = $pParamHash['content_status_id']; } } } $pParamHash['field_changed'] = empty( $pParamHash['content_id'] ) || (!empty($this->mInfo["data"]) && !empty($pParamHash["edit"]) && (md5($this->mInfo["data"]) != md5($pParamHash["edit"]))) || (!empty($pParamHash["title"]) && !empty($this->mInfo["title"]) && (md5($this->mInfo["title"]) != md5($pParamHash["title"]))) || (!empty($pParamHash["edit_comment"]) && !empty($this->mInfo["edit_comment"]) && (md5($this->mInfo["edit_comment"]) != md5($pParamHash["edit_comment"]))); // check some lengths, if too long, then truncate if( !empty( $pParamHash['title'] ) ) { $pParamHash['content_store']['title'] = substr( preg_replace( '/:space:+/m', ' ', trim( $pParamHash['title'] ) ), 0, BIT_CONTENT_MAX_TITLE_LEN ); } elseif( isset( $pParamHash['title'] ) ) { $pParamHash['content_store']['title'] = null; } // get the lang code from $_REQUEST if it's not set if( !empty( $pParamHash['lang_code'] ) && in_array( $pParamHash['lang_code'], array_keys( $gBitLanguage->mLanguageList ) ) ) { $pParamHash['content_store']['lang_code'] = $pParamHash['lang_code']; } elseif( !empty( $_REQUEST['i18n']['lang_code'] ) && in_array( $_REQUEST['i18n']['lang_code'], array_keys( $gBitLanguage->mLanguageList ) ) ) { $pParamHash['content_store']['lang_code'] = $_REQUEST['i18n']['lang_code']; } $pParamHash['content_store']['last_modified'] = !empty( $pParamHash['last_modified'] ) ? $pParamHash['last_modified'] : $gBitSystem->getUTCTime(); if( !empty( $pParamHash['event_time'] ) ) { $pParamHash['content_store']['event_time'] = $pParamHash['event_time']; } // WARNING: Assume WIKI if t if( !empty( $pParamHash['content_id'] ) ) { // do NOT allow changing of content_type_guid in update for safety of overridden secondary classes (like BitBook ) unset( $pParamHash['content_store']['content_type_guid'] ); } elseif( empty( $pParamHash['content_type_guid'] ) ) { $this->mErrors['content_type'] = KernelTools::tra( 'System Error: Unknown content type' ); } else { $pParamHash['content_store']['content_type_guid'] = $pParamHash['content_type_guid']; } // setup some required defaults if not defined if( empty( $pParamHash['ip'] ) ) { $pParamHash['ip'] = empty( $_SERVER["REMOTE_ADDR"] ) ? '127.0.0.1' : $_SERVER["REMOTE_ADDR"]; } $pParamHash['content_store']['ip'] = $pParamHash['ip']; if( !@$this->verifyId( $pParamHash['modifier_user_id'] ?? '' ) ) { $pParamHash['modifier_user_id'] = $gBitUser->getUserId(); } $pParamHash['content_store']['modifier_user_id'] = $pParamHash['modifier_user_id']; if( empty( $pParamHash['format_guid'] ) ) { $pParamHash['format_guid'] = $gBitSystem->getConfig( 'default_format', 'tikiwiki' ); } $pParamHash['content_store']['format_guid'] = $pParamHash['format_guid']; if( !empty( $pParamHash['hits'] ) ) { $pParamHash['content_store']['hits'] = $pParamHash['hits'] + 1; $pParamHash['content_store']['last_hit'] = $gBitSystem->getUTCTime(); } if( !empty( $pParamHash['edit'] ) && $func = $gLibertySystem->getPluginFunction( $pParamHash['format_guid'], 'verify_function' ) ) { $error = $func( $pParamHash ); if( $error ) { $this->mErrors['format'] = $error; } } if( !empty( $pParamHash['content_store']['data'] )) { $this->filterData( $pParamHash['content_store']['data'], $pParamHash['content_store'], 'prestore' ); } else { // someone has deleted the data entirely - common for fisheye $pParamHash['content_store']['data'] = null; } $pParamHash['content_store']['format_guid'] = $pParamHash['format_guid']; $pParamHash['content_store']['version'] = !BitBase::verifyId( $this->mInfo['version'] ?? 0 ) ? 1 : $this->mInfo['version'] + 1; // search related stuff if ( ( !(isset($this->mInfo['no_index']) and $this->mInfo['no_index'] == true ) ) and !isset($this->mInfo['index_data']) ) { $this->mInfo['index_data'] = ""; if ( isset($pParamHash["title"]) ) $this->mInfo['index_data'] .= $pParamHash["title"] . ' '; if ( isset($pParamHash["author_name"]) ) $this->mInfo['index_data'] .= $pParamHash["author_name"] . ' '; if ( isset($pParamHash["edit"]) ) $this->mInfo['index_data'] .= $pParamHash["edit"]; } // content preferences $prefs = []; if( $gBitUser->hasPermission( 'p_liberty_enter_html' ) ) { $prefs[] = 'content_enter_html'; } foreach( $prefs as $pref ) { $pParamHash['preferences_store'][$pref] = !empty( $pParamHash['preferences'][$pref] ) ? $pParamHash['preferences'][$pref] : null; } $pParamHash['data_store']['summary'] = !empty( $pParamHash['summary'] ) ? $pParamHash['summary'] : null ; // call verify service to see if any services have errors $this->invokeServices( 'content_verify_function', $pParamHash ); return count( $this->mErrors ) == 0; } /** * Create a new content object or update an existing one * * @param array Array of content data to be stored
* @return bool true on success, false if store could not occur. If false, $this->mErrors will have reason why * See verify for details of the values required */ public function store( array &$pParamHash ): bool { global $gLibertySystem; if( LibertyContent::verify( $pParamHash ) ) { $this->clearFromCache(); $this->StartTrans(); $table = BIT_DB_PREFIX."liberty_content"; if( !$this->verifyIdParameter( $pParamHash, 'content_id' ) ) { // make sure some variables are stuff in case services need getObjectType, mContentId, etc... $this->mContentId = $pParamHash['content_id'] = $pParamHash['content_store']['content_id'] = $this->mDb->GenID( 'liberty_content_id_seq' ); $this->mContentTypeGuid = $this->mInfo['content_type_guid'] = $pParamHash['content_type_guid']; $result = $this->mDb->associateInsert( $table, $pParamHash['content_store'] ); $this->mLogs['content_store'] = "Created"; } else { if( !empty( $pParamHash['content_store']['title'] ) && !empty( $this->mInfo['title'] ) && $pParamHash['content_store']['title'] != $this->mInfo['title'] ) { $this->mLogs['rename_page'] = "Renamed from {$this->mInfo['title']} to {$pParamHash['content_store']['title']}."; } $result = $this->mDb->associateUpdate( $table, $pParamHash['content_store'], [ "content_id" => $pParamHash['content_id'] ] ); $this->mLogs['content_store'] = "Updated"; } if( !empty( $pParamHash['force_history'] ) || ( empty( $pParamHash['minor'] ) && $this->getField( 'version' ) && $pParamHash['field_changed'] )) { if( empty( $pParamHash['has_no_history'] ) ) { $this->storeHistory(); } //$action = "Created"; //$mailEvents = 'wiki_page_changes'; } $this->storeAliases( $pParamHash ); $this->invokeServices( 'content_store_function', $pParamHash ); // Call the formatter's save if( !empty( $pParamHash['content_store']['data'] )) { if( $func = $gLibertySystem->getPluginFunction( $pParamHash['format_guid'], 'store_function' ) ) { $ret = $func( $pParamHash ); } // post store filter - this is needed to deal with filters that need the content_id on the first save $this->filterData( $pParamHash['content_store']['data'], $pParamHash['content_store'], 'poststore' ); } LibertyContent::expungeCacheFile( $pParamHash['content_id'] ); // store data foreach( $pParamHash['data_store'] AS $dataType => $data ) { $this->storeData( $data, $dataType ); } // store content preferences if( !empty( $pParamHash['preferences_store'] ) ) { foreach( $pParamHash['preferences_store'] as $pref => $value ) { $this->storePreference( $pref, $value ); } } // store hits and last hit if( !empty( $pParamHash['content_store']['hits'] ) ) { $this->setHits($pParamHash['content_store']['hits'], $pParamHash['content_store']['last_hit']); } // store any messages in the logs $this->storeActionLog( $pParamHash ); $this->CompleteTrans(); } return count( $this->mErrors ) == 0; } /** * Delete comment entries relating to the content object * * @access public * @return void */ public function expungeComments() { // Delete all comments associated with this piece of content $query = "SELECT `comment_id` FROM `".BIT_DB_PREFIX."liberty_comments` WHERE `root_id` = ?"; if( $commentIds = $this->mDb->getCol($query, [ $this->mContentId ] ) ) { foreach ($commentIds as $commentId) { $tmpComment = new LibertyComment($commentId); $tmpComment->expunge(); } } parent::expunge(); } /** * Delete content object and all related records * * @return bool */ public function expunge(): bool { global $gBitSystem, $gLibertySystem; if( $this->isValid() ) { $this->StartTrans(); $this->expungeComments(); // services, filters and cache $this->invokeServices( 'content_expunge_function', $this ); if( $this->getField( 'format_guid' ) && $func = $gLibertySystem->getPluginFunction( $this->getField( 'format_guid' ), 'expunge_function' ) ) { $func( $this->mContentId ); } $this->filterData( $this->mInfo['data'], $this->mInfo, 'expunge' ); LibertyContent::expungeCacheFile( $this->mContentId ); // remove favorites - this probably should be a content_expunge_function in users $this->mDb->query( "DELETE FROM `".BIT_DB_PREFIX."users_favorites_map` WHERE `favorite_content_id`=?", [ $this->mContentId ] ); // remove entries in the history $this->expungeVersion(); // Remove individual permissions for this object if they exist $query = "delete from `".BIT_DB_PREFIX."liberty_content_permissions` where `content_id`=?"; $result = $this->mDb->query( $query, [ $this->mContentId ] ); // Remove aliases $this->mDb->query( "DELETE FROM `".BIT_DB_PREFIX."liberty_aliases` WHERE `content_id`=?", [ $this->mContentId ] ); // Remove structures // it's not this simple. what about orphans? needs real work. :( xoxo - spider // $query = "DELETE FROM `".BIT_DB_PREFIX."liberty_structures` WHERE `content_id` = ?"; // $result = $this->mDb->query( $query, [ $this->mContentId ] ); // Remove any queued data processing (images, movies, etc.) $query = "DELETE FROM `".BIT_DB_PREFIX."liberty_process_queue` WHERE `content_id` = ?"; $result = $this->mDb->query( $query, [ $this->mContentId ] ); // Remove data $query = "DELETE FROM `".BIT_DB_PREFIX."liberty_content_data` WHERE `content_id` = ?"; $result = $this->mDb->query( $query, [ $this->mContentId ] ); // Remove hits $query = "DELETE FROM `".BIT_DB_PREFIX."liberty_content_hits` WHERE `content_id` = ?"; $result = $this->mDb->query( $query, [ $this->mContentId ] ); // Remove content preferences $query = "DELETE FROM `".BIT_DB_PREFIX."liberty_content_prefs` WHERE `content_id` = ?"; $result = $this->mDb->query( $query, [ $this->mContentId ] ); // Remove content links $query = "DELETE FROM `".BIT_DB_PREFIX."liberty_content_links` WHERE `to_content_id` = ? or `from_content_id` = ?"; $result = $this->mDb->query( $query, [ $this->mContentId, $this->mContentId ] ); // Remove xref records owned by this content $query = "DELETE FROM `".BIT_DB_PREFIX."liberty_xref` WHERE `content_id` = ?"; $result = $this->mDb->query( $query, [ $this->mContentId ] ); // Remove content $query = "DELETE FROM `".BIT_DB_PREFIX."liberty_content` WHERE `content_id` = ?"; $result = $this->mDb->query( $query, [ $this->mContentId ] ); $this->mLogs['content_expunge'] = "Deleted"; $this->storeActionLog(); $this->CompleteTrans(); parent::expunge(); } return true; } /** * storeAliases will store aliases to a given content item * * @access public * @return bool true on success, false on failure - mErrors will contain reason for failure */ public function storeAliases( $pParamHash ) { $ret = false; if( $this->isValid() && isset( $pParamHash['alias_string']) ) { $this->mDb->query( "DELETE FROM `".BIT_DB_PREFIX."liberty_aliases` WHERE `content_id`=?", [ $this->mContentId ] ); $trimmedAliases = trim( $pParamHash['alias_string'] ); if( !empty( $trimmedAliases ) && $aliases = explode( "\n", $trimmedAliases ) ) { foreach( $aliases as $a ) { $this->mDb->query( "INSERT INTO `".BIT_DB_PREFIX."liberty_aliases` (`content_id`, `alias_title`) VALUES (?,?)", [ $this->mContentId, trim( $a ) ] ); } } $ret = true; } return $ret; } /** * storeHistory will store the previous data into the history table for reference * * @access public * @return bool true on success, false on failure - mErrors will contain reason for failure */ public function storeHistory() { global $gBitSystem; $ret = false; if( $this->isValid() ) { $storeHash = [ "content_id" => $this->mContentId, "version" => $this->getField( "version" ), "last_modified" => $this->getField( "last_modified" ), "user_id" => $this->getField( "modifier_user_id" ), "ip" => $this->getField( "ip" ), "data" => $this->getField( "data" ), "summary" => $this->getField( "summary" ), "history_comment" => (string)substr( $this->getField( "edit_comment", '' ), 0, 200 ), "format_guid" => $this->getField( "format_guid", $gBitSystem->getConfig( "default_format", "tikiwiki" )), ]; $this->mDb->associateInsert( BIT_DB_PREFIX."liberty_content_history", $storeHash ); $ret = true; } return $ret; } /** * Get count of the number of historic records for the page * * @access public * @return int count */ public function getHistoryCount() { $ret = null; if( $this->isValid() ) { $query = " SELECT COUNT(*) AS `hcount` FROM `".BIT_DB_PREFIX."liberty_content_history` WHERE `content_id` = ?"; $rs = $this->mDb->query($query, [ $this->mContentId ]); $ret = $rs->fields['hcount']; } return $ret; } /** * Get complete set of historical data in order to display a given wiki page version * * @param array $pVersion * @param array $pUserId * @param int $pOffset * @param array $max_records * @access public * @return array of mInfo data */ public function getHistory( $pVersion=null, $pUserId=null, $pOffset = 0, $max_records = -1 ) { $ret = null; $cant = 0; if( $this->isValid() ) { global $gBitSystem; $selectSql = ''; $joinSql = ''; $whereSql = ''; $bindVars = []; $this->getServicesSql( 'content_list_history_sql_function', $selectSql, $joinSql, $whereSql, $bindVars ); $versionSql = ''; if( BitBase::verifyId( $pUserId ) ) { $bindVars[] = $pUserId; $whereSql .= ' th.`user_id`=? '; } else { $bindVars[] = $this->mContentId; $whereSql .= ' th.`content_id`=? '; } if( BitBase::verifyId( $pVersion ) ) { array_push( $bindVars, $pVersion ); $versionSql = ' AND th.`version`=? '; } $query = "SELECT COUNT(*) AS `hcount` FROM `".BIT_DB_PREFIX."liberty_content_history` WHERE `content_id` = ?"; $rs = $this->mDb->query($query, [ $this->mContentId ]); $cant = $rs->fields['hcount']; # Check for offset out of range if ( $pOffset < 0 ) { $pOffset = 0; } elseif ( $pOffset > $cant ) { $lastPageNumber = ceil ( $cant / $max_records ) - 1; $pOffset = $max_records * $lastPageNumber; } $query = "SELECT lc.`title`, th.*, uue.`login` AS modifier_user, uue.`real_name` AS modifier_real_name, uuc.`login` AS creator_user, uuc.`real_name` AS creator_real_name $selectSql FROM `".BIT_DB_PREFIX."liberty_content_history` th INNER JOIN `".BIT_DB_PREFIX."liberty_content` lc ON (lc.`content_id` = th.`content_id`) LEFT JOIN `".BIT_DB_PREFIX."users_users` uue ON (uue.`user_id` = th.`user_id`) LEFT JOIN `".BIT_DB_PREFIX."users_users` uuc ON (uuc.`user_id` = lc.`user_id`) $joinSql WHERE $whereSql $versionSql order by th.`version` desc"; $result = $this->mDb->query( $query, $bindVars, $max_records, $pOffset ); $data = []; while( !$result->EOF ) { $aux = $result->fields; $aux['creator'] = $aux['creator_real_name'] ?? $aux['creator_user']; $aux['editor'] = $aux['modifier_real_name'] ?? $aux['modifier_user']; $data[] = $aux; //array_push( $ret, $aux ); $result->MoveNext(); } } // Temporary patch to get a $pListHash array for the output // this needs to be tidied on the input side // TODO: update this to work like newer getList methods $pListHash = []; $pListHash["data"] = $data; $pListHash["cant"] = $cant; $pListHash["max_records"] = $max_records; $pListHash["offset"] = $pOffset; $pListHash["find"] = null; $pListHash["sort_mode"] = null; LibertyContent::postGetList( $pListHash ); return $pListHash; } /** * Removes last version of the page (from pages) if theres some * version in the liberty_content_history then the last version becomes the actual version * * @param string $pComment * @access public * @return void */ public function removeLastVersion( $pComment = '' ): void { if( $this->isValid() ) { global $gBitSystem; $this->expungeCacheFile($this->mContentId); $query = "select * from `".BIT_DB_PREFIX."liberty_content_history` where `content_id`=? order by ".$this->convertSortMode("last_modified_desc"); $result = $this->mDb->query($query, [ $this->mContentId ] ); if ($result->numRows()) { // We have a version $res = $result->fetchRow(); $this->rollbackVersion( $res["version"] ); $this->expungeVersion( $res["version"] ); } // @TODO Build missing function // $this->remove_all_versions($page); $action = "Removed last version"; $t = $gBitSystem->getUTCTime(); $query = "insert into `".BIT_DB_PREFIX."liberty_action_log`( `log_message`, `content_id`, `last_modified`, `user_id`, `ip`, `error_message`) values( ?, ?, ?, ?, ?, ?)"; $result = $this->mDb->query( $query, [ $action, $this->mContentId, $t, ROOT_USER_ID, $_SERVER["REMOTE_ADDR"], $pComment ]); } } /** * Roll back to a specific version of a page * @param int pVersion Version number to roll back to * @param string pComment Comment text to be added to the action log * @return bool true if completed successfully */ public function rollbackVersion( int $pVersion, string $pComment = '' ): bool { $ret = false; if( $this->isValid() ) { global $gBitUser,$gBitSystem; $this->StartTrans(); // JHT - cache invalidation appears to be handled by store function - so don't need to do it here $query = "select lch.*, lch.`user_id` AS modifier_user_id, lch.`data` AS `edit` from `".BIT_DB_PREFIX."liberty_content_history` lch where lch.`content_id`=? and lch.`version`=?"; if( $res = $this->mDb->getRow($query, [ $this->mContentId, $pVersion ] ) ) { $res['edit_comment'] = 'Rollback to version '.$pVersion.' by '.$gBitUser->getDisplayName(); if (!empty($pComment)) { $res['edit_comment'] .=": $pComment"; } // JHT 2005-06-19_15:22:18 // set ['force_history'] to // make sure we don't destory current content without leaving a copy in history // if rollback can destroy the current page version, it can be used // maliciously $res['force_history'] = 1; // JHT 2005-10-16_22:21:10 // title must be set or store fails // we use current page name $res['title'] = $this->getTitle(); if( $this->store( $res ) ) { $ret = true; } $this->CompleteTrans(); } else { $this->mDb->RollbackTrans(); } } return $ret; } /** * Removes a specific version of a page * * @param int $pVersion Version number to roll back to * @param string $pComment Comment text to be added to the action log * @return bool true if completed successfully */ public function expungeVersion( int $pVersion = 0, $pComment = '' ): bool { global $gBitUser; $ret = false; if( $this->isValid() ) { $this->StartTrans(); $bindVars = [ $this->mContentId ]; $versionSql = ''; if( $pVersion ) { $versionSql = " and `version`=? "; array_push( $bindVars, $pVersion ); } $hasRows = $this->mDb->getOne( "SELECT COUNT(`version`) FROM `".BIT_DB_PREFIX."liberty_content_history` WHERE `content_id`=? $versionSql ", $bindVars ); $query = "DELETE FROM `".BIT_DB_PREFIX."liberty_content_history` WHERE `content_id`=? $versionSql "; $result = $this->mDb->query( $query, $bindVars ); if( $hasRows ) { global $gBitSystem; $action = "Removed version $pVersion"; $t = $gBitSystem->getUTCTime(); $query = "INSERT INTO `".BIT_DB_PREFIX."liberty_action_log` (`log_message`,`content_id`,`last_modified`,`user_id`,`ip`,`error_message`) VALUES (?,?,?,?,?,?)"; $result = $this->mDb->query($query, [ $action, $this->mContentId, $t, $gBitUser->mUserId, $_SERVER["REMOTE_ADDR"], $pComment ]); $ret = true; } $this->CompleteTrans(); } return $ret; } public function exportList( array $pList ): array { $ret = []; foreach( $pList as $keyId=>$hash ) { $content = static::getLibertyObject( $keyId ); $ret[$keyId] = $content->exportHash(); } return $ret; } /** * Create an export hash from the data * * @access public * @return array export data */ public function exportHash() { $ret = []; if( $this->isValid() ) { $ret = [ 'content_type_guid' => $this->getContentType(), 'content_type' => $this->getContentTypeName(), 'content_id' => $this->mContentId, 'title' => $this->getTitle(), 'display_uri' => $this->getDisplayUri(), 'display_url' => $this->getDisplayUrl(), 'date_created' => date( \DateTime::W3C, $this->getField( 'created' ) ), 'date_last_modified' => date( \DateTime::W3C, $this->getField( 'last_modified' ) ), ]; $keys = $this->invokeServices( 'content_export_keys_function', $pList ); foreach( $keys as $field ) { if( $value = $this->getField( $field ) ) { $ret[$field] = $value; } } } return $ret; } /** * Check mContentId to establish if the object has been loaded with a valid record */ public function isValid() { return BitBase::verifyId( $this->mContentId ); } /** * Check permissions to establish if user has permission to view the object * Should be provided by the decendent package */ public function isViewable($pContentId = null) { return true; } /** * Check permissions to establish if user has permission to edit the object * Should be provided by the decendent package */ public function isEditable() { return false; } /** * Check permissions to establish if user has permission to admin the object * That would include permission to delete an object or change it's permissions * Should be provided by the decendent package */ public function isAdminable($pContentId = null) { return false; } /** * Check user_id to establish if the object that has been loaded was created by the current user * @param $pParamHash optionally pass in the hash to check against * @return bool true if user owns the content */ public function isOwner( $pParamHash = null ) { global $gBitUser; if( !empty($pParamHash['user_id']) && BitBase::verifyId( $pParamHash['user_id'] ) ) { $user_id = $pParamHash['user_id']; } elseif( $this->isValid() && @$this->verifyId( $this->mInfo['user_id'] ?? 0 ) ) { $user_id = $this->mInfo['user_id']; } else { $user_id = ANONYMOUS_USER_ID; } return $user_id != ANONYMOUS_USER_ID && BitBase::verifyId( $user_id ) && $user_id == $gBitUser->mUserId; } /** * Check if content matches content type GUID - must also be a valid content object, it will not work for generic content class */ public function isContentType( $pContentGuid ) { global $gBitUser; return $this->isValid() && !empty( $this->mInfo['content_type_guid'] ) && $this->mInfo['content_type_guid'] == $pContentGuid; } /** * Check permissions to establish if user has permission to access the object */ public function verifyAccessControl() { if( $this->isValid() ) { $this->invokeServices( 'content_verify_access' ); } } /** * Set up access to services used by the object */ public function invokeServices( $pServiceFunction, &$pFunctionParam=null ) { global $gLibertySystem; $errors = []; // Invoke any services store functions such as categorization or access control if( $serviceFunctions = $gLibertySystem->getServiceValues( $pServiceFunction ) ) { foreach ( $serviceFunctions as $func ) { $func = "\\Bitweaver\\Liberty\\$func"; if( function_exists( $func ) ) { if( $errors = $func( $this, $pFunctionParam ) ) { $this->mErrors = array_merge( $this->mErrors, $errors ); } } } } return $errors; } /** * check if a service is active for this content type * requires package LCConfig * provisional method until LCConfig package is integrated into the core */ public function hasService(){ global $gBitSystem; $ret = true; // we return true by default to preserve legacy service opperation which has no content type preferences /* if( $gBitSystem->isPackageActive( 'lcconfig' ) ){ // LCConfig is a singleton class $LCConfig = LCConfig::getInstance(); // LCConfig negates services by content type // if result is not 'n' then service should apply to this content type if( $LCConfig->getConfig( 'service_'.$pServiceGuid, $this->mContentTypeGuid ) == 'n' ){ $ret = false; } } */ return $ret; } /** * check if a service is required for this content type * requires package LCConfig * provisional method until LCConfig package is integrated into the core */ /* public function isServiceRequired( $pServiceGuid ){ global $gBitSystem; $ret = true; // we return true by default to preserve legacy service opperation which has no content type preferences if( $gBitSystem->isPackageActive( 'lcconfig' ) ){ // LCConfig is a singleton class $LCConfig = LCConfig::getInstance(); return ( $LCConfig->getConfig( 'service_'.$pServiceGuid, $this->mContentTypeGuid ) == 'required' ); } return $ret; } */ /** * Default liberty sql for joining a content object table to liberty. * We are proposing a new way of building queries here where we build up everything in a hash with implicit AND over all * where clauses and then do an array_merge and concatenation in a single function at the end. See convertQueryHash for details. * * This is an example current, and would be invoked in getList * $queryHash = [ 'summary', 'users', 'hits', 'avatar', 'primary' ], [ 'select' => ['sql' => $selectSql ], 'join' => [ 'sql' => $joinSql ], 'where' => [ 'sql' => $whereSql, 'var' => $bindVars ] ); * $this->getLibertySql( 'bp.`content_id`', $queryHash); */ public function getLibertySql( $pJoinColumn, &$pQueryHash, $pJoins = null, $pServiceFunction = null, $pObject = null, $pParamHash = null ) { $pQueryHash['select']['sql'][] = "lc.*"; $pQueryHash['join']['sql'][] = " INNER JOIN `".BIT_DB_PREFIX."liberty_content` lc ON( lc.`content_id` = $pJoinColumn )"; if( empty( $pJoins ) || in_array( 'summary', $pJoins )) { $pQueryHash['select']['sql'][] = "lcds.`data` AS `summary`"; $pQueryHash['join']['sql'][] = " LEFT OUTER JOIN `".BIT_DB_PREFIX."liberty_content_data` lcds ON( lc.`content_id` = lcds.`content_id` AND lcds.`data_type` = ? )"; $pQueryHash['join']['var'][] = 'summary'; } if( empty( $pJoins ) || in_array( 'hits', $pJoins )) { $pQueryHash['select']['sql'][] = "lch.`hits`, lch.`last_hit`"; $pQueryHash['join']['sql'][] = " LEFT OUTER JOIN `".BIT_DB_PREFIX."liberty_content_hits` lch ON( lc.`content_id` = lch.`content_id` )"; } if( empty( $pJoins ) || in_array( 'users', $pJoins )) { $pQueryHash['select']['sql'][] = " uu.`email` AS creator_email, uu.`login` AS creator_user, uu.`real_name` AS creator_real_name, uue.`email` AS modifier_email, uue.`login` AS modifier_user, uue.`real_name` AS modifier_real_name"; $pQueryHash['join']['sql'][] = " INNER JOIN `".BIT_DB_PREFIX."users_users` uu ON( uu.`user_id` = lc.`user_id` ) LEFT OUTER JOIN `".BIT_DB_PREFIX."users_users` uue ON( uue.`user_id` = lc.`modifier_user_id` )"; } if( empty( $pJoins ) || in_array( 'avatar', $pJoins )) { $pQueryHash['select']['sql'][] = "ulf.`file_name` AS `avatar_file_name`, ulf.`mime_type` AS `avatar_mime_type`, ula.`attachment_id` AS `avatar_attachment_id`"; $pQueryHash['join']['sql'][] = " LEFT OUTER JOIN `".BIT_DB_PREFIX."liberty_attachments` ula ON( uu.`user_id` = ula.`user_id` AND ula.`attachment_id` = uu.`avatar_attachment_id` ) LEFT OUTER JOIN `".BIT_DB_PREFIX."liberty_files` ulf ON( ulf.`file_id` = ula.`foreign_id` )"; } if( empty( $pJoins ) || in_array( 'primary', $pJoins )) { $pQueryHash['select']['sql'][] = "pla.`attachment_id` AS `primary_attachment_id`, plf.`file_name` AS `primary_file_name`, plf.`mime_type` AS `primary_mime_type`"; $pQueryHash['join']['sql'][] = " LEFT OUTER JOIN `".BIT_DB_PREFIX."liberty_attachments` pla ON( pla.`content_id` = lc.`content_id` AND pla.`is_primary` = 'y' ) LEFT OUTER JOIN `".BIT_DB_PREFIX."liberty_files` plf ON( plf.`file_id` = pla.`foreign_id` )"; } if( !empty( $pServiceFunction )) { $this->getServicesSql2( $pServiceFunction, $pQueryHash, $pObject, $pParamHash ); } } /** * getServicesSql2 * * @param string $pServiceFunction * @param array $pQueryHash * @param array $pObject * @param array $pParamHash * @access public * @return void * @TODO this function still contains legacy code. * @TODO rename this function to getServicesSql has been weened out */ public function getServicesSql2( $pServiceFunction, &$pQueryHash, $pObject = null, $pParamHash = null ) { global $gLibertySystem; if( $loadFuncs = $gLibertySystem->getServiceValues( $pServiceFunction ) ) { // TODO: clear out this legacy code $pQueryHash['service_select_sql'] = $pQueryHash['service_join_sql'] = $pQueryHash['service_where_sql'] = ''; foreach( $loadFuncs as $func ) { if( function_exists( $func ) ) { $queryHash = !empty( $pObject ) && is_object( $pObject ) ? $func( $pObject, $pParamHash ) : $func( $this, $pParamHash ); // work out if we're using the old services sql method or the new one if( !empty( $queryHash['select'] ) || !empty( $queryHash['from'] ) || !empty( $queryHash['join'] ) || !empty( $queryHash['where'] )) { // we're using the new method $pQueryHash = array_merge_recursive( $pQueryHash, $queryHash ); } else { // TODO: clean out this legacy code {{{ // old method: warn the developer //deprecated( 'This service is still using the old LibertyContent::getServicesSql() method. Please update the service to use the new SQL hash method' ); if( !empty( $queryHash['select_sql'] )) { $pQueryHash['service_select_sql'] .= $queryHash['select_sql']; } if( !empty( $queryHash['join_sql'] )) { $pQueryHash['service_join_sql'] .= $queryHash['join_sql']; } if( !empty( $queryHash['where_sql'] )) { $pQueryHash['service_where_sql'] .= $queryHash['where_sql']; } if( !empty( $queryHash['bind_vars'] ) ) { $pQueryHash['service_bind_vars'] = is_array( $pQueryHash['service_bind_vars'] ) ? array_merge( $pQueryHash['service_bind_vars'], $queryHash['bind_vars'] ) : $queryHash['bind_vars']; } } } } } } /** * Convert a built up pQueryHash into a single query string and set of bind variables. * * A pQueryHash is an array with required keys select and from, and optional keys join, where and order. * Each key other than order should be an array with an 'sql' key which points to an array with statements. * Statements should not include the keywords to start them excluding join statements nor should they * include trailing delimeters such as commas as the conversion adds these where required. * All where statments are automatically ANDed together. * Each key other than order can optionally have a 'vars' key which points to an array with bind variables. * The order key can either be an array or a single value. convertSortmode is automatically called on each order * statement and built into the ORDER BY clause with delimeters where required. * * @return void Results come back in $pQueryHash['query'] $pQueryHash['bind_vars'] and $pQueryHash['query_count'] if requested * @TODO this function still contains legacy code. */ public function convertQueryHash( &$pQueryHash, $pCountQuery = false ) { global $gBitSystem; // initiate some variables if( empty( $pQueryHash['query'] )) { $pQueryHash['query'] = ''; } if( empty( $pQueryHash['query_count'] )) { $pQueryHash['query_count'] = ''; } if( empty( $pQueryHash['bind_vars'] )) { $pQueryHash['bind_vars'] = []; } // Build up all the parts of the query $queryParts = [ 'select', 'from', 'join', 'where' ]; foreach( $queryParts as $part ) { if( !empty( $pQueryHash[$part] ) && !empty( $pQueryHash[$part]['sql'] )) { // Add the required keyword -- joins include their own if( $part != 'join' ) { $pQueryHash['query'] .= strtoupper( " $part " ); if( $pCountQuery ) { $pQueryHash['query_count'] .= strtoupper( " $part " ); } } // Add the count for the count query if( $pCountQuery && $part == 'select' ) { $pQueryHash['query_count'] .= 'COUNT( '; } $first = true; foreach( $pQueryHash[$part]['sql'] as $sql ) { if( !$first ) { // WHERE clauses have an implicit AND over all terms if( $part == 'where' ) { $pQueryHash['query'] .= " AND "; if( $pCountQuery ) { $pQueryHash['query_count'] .= " AND "; } } elseif( $part == 'select' || $part == 'from' ) { $pQueryHash['query'] .= ", "; if( $pCountQuery ) { $pQueryHash['query_count'] .= ", "; } } } else { $first = false; } $pQueryHash['query'] .= $sql; if( $pCountQuery ) { $pQueryHash['query_count'] .= $sql; } } // Close the count for the count query if( $pCountQuery && $part == 'select' ) { $pQueryHash['query_count'] .= ' )'; } if( !empty( $pQueryHash[$part]['var'] )) { $pQueryHash['bind_vars'] = array_merge( $pQueryHash['bind_vars'], $pQueryHash[$part]['var'] ); } } // TODO: clean out this legacy code {{{ // append old style serivce sql arguments // since we don't allow bind_vars in the old services style, we can append everything here and then later on add the bind vars if( !empty( $pQueryHash['service_'.$part.'_sql'] )) { $pQueryHash['query'] .= $pQueryHash['service_'.$part.'_sql']; if( $pCountQuery ) { $pQueryHash['query_count'] .= $pQueryHash['service_'.$part.'_sql']; } } // }}} } // TODO: clean out this legacy code {{{ // append legacy service bind vars if( !empty( $pQueryHash['service_bind_vars'] )) { $pQueryHash['bind_vars'] = array_merge( $pQueryHash['bind_vars'], $pQueryHash['service_bind_vars'] ); } /// }}} // Order can be a single value or an array of values all of which get passed to convertSortmode if( !empty( $pQueryHash['order'] )) { if( is_array( $pQueryHash['order'] )) { $first = true; foreach( $pQueryHash['order'] as $order ) { if( !$first ) { $pQueryHash['query'] .= ', '; } else { $pQueryHash['query'] .= ' ORDER BY '; $first = false; } $pQueryHash['query'] .= $gBitSystem->mDb->convertSortmode( $order ); } } else { $pQueryHash['query'] .= ' ORDER BY '.$gBitSystem->mDb->convertSortmode( $pQueryHash['order'] ); } } } /** * Set up SQL strings for services used by the object * TODO: set this function deprecated and eventually nuke it */ public static function getServicesSql( $pServiceFunction, &$pSelectSql, &$pJoinSql, &$pWhereSql, &$pBindVars, $pObject = null, &$pParamHash = null ) { global $gLibertySystem; if( $loadFuncs = $gLibertySystem->getServiceValues( $pServiceFunction ) ) { foreach( $loadFuncs as $func ) { $func = "\\Bitweaver\\Liberty\\$func"; if( function_exists( $func ) ) { $loadHash = !empty( $pObject ) && is_object( $pObject ) ? $func( $pObject, $pParamHash ) : $func( !empty( $pObject ) ? $pObject : null, $pParamHash ); if( !empty( $loadHash['select_sql'] ) ) { $pSelectSql .= $loadHash['select_sql']; } if( !empty( $loadHash['join_sql'] ) ) { $pJoinSql .= $loadHash['join_sql']; } if( !empty( $loadHash['where_sql'] ) ) { $pWhereSql .= $loadHash['where_sql']; } if( !empty( $loadHash['bind_vars'] ) ) { $pBindVars = is_array( $pBindVars ) ? $pBindVars = array_merge( $pBindVars, $loadHash['bind_vars'] ) : $loadHash['bind_vars']; } } } } } // -------------------------------- Content Permission Functions /** * Check to see if the loaded content has individually assigned permissions * * @access public * @return int Number of custom assigned permissions set for the loaded content item */ public function hasUserPermissions() { $ret = false; if( $this->isValid() ) { $ret = $this->mDb->getOne( "SELECT COUNT(`perm_name`) FROM `".BIT_DB_PREFIX."liberty_content_permissions` WHERE `content_id` = ?", [ $this->mContentId ]); } return $ret; } /** * getContentPermissionsSql * * @param string $pPermName * @param string $pSelectSql * @param string $pJoinSql * @param string $pWhereSql * @param array $pBindVars * @return void set of 3 strings and array of variables updated by reference */ public function getContentPermissionsSql( $pPermName, &$pSelectSql, &$pJoinSql, &$pWhereSql, &$pBindVars ) { global $gBitUser; $pJoinSql .= " LEFT OUTER JOIN `".BIT_DB_PREFIX."liberty_content_permissions` lcperm ON (lc.`content_id`=lcperm.`content_id`) LEFT OUTER JOIN `".BIT_DB_PREFIX."users_roles_map` urm ON (urm.`role_id`=lcperm.`role_id`) "; $pWhereSql .= " OR (lcperm.perm_name=? AND (urm.user_id=? OR urm.user_id=?)) "; $pBindVars[] = $pPermName; $pBindVars[] = $gBitUser->mUserId; $pBindVars[] = ANONYMOUS_USER_ID; } /** * getContentListPermissionsSql * * @param array $pPermName * @param array $pSelectSql * @param array $pJoinSql * @param array $pWhereSql * @param array $pBindVars * @return void set of 3 strings and array of variables updated by reference */ public static function getContentListPermissionsSql( $pPermName, &$pSelectSql, &$pJoinSql, &$pWhereSql, &$pBindVars ) { global $gBitUser; $pJoinSql .= " LEFT OUTER JOIN `".BIT_DB_PREFIX."liberty_content_permissions` lcperm ON (lc.`content_id`=lcperm.`content_id`) LEFT OUTER JOIN `".BIT_DB_PREFIX."users_roles_map` urm ON (urm.`role_id`=lcperm.`role_id`) "; $pWhereSql .= " AND ( lcperm.perm_name IS null OR ( lcperm.perm_name=? AND urm.user_id=? AND ( (lcperm.is_revoked !=? OR lcperm.is_revoked IS null) OR lc.`user_id`=? ) ) )"; $pBindVars[] = $pPermName; $pBindVars[] = $gBitUser->mUserId; $pBindVars[] = "y"; $pBindVars[] = $gBitUser->mUserId; } /** * Check is a user has permission to access the object * * @param array $pParamHash * @var integer User Identifier * @param integer Content Itentifier * @param string Content Type GUID * @param string Name of the permission * @return bool true if access is allowed */ public function checkContentPermission( $pParamHash ) { global $gBitUser; $ret = false; if( !empty( $this->mAdminContentPerm ) && $gBitUser->hasPermission( $this->mAdminContentPerm ) ) { // content admin shortcut $ret = true; } else { $selectSql = ''; $joinSql = ''; $whereSql = ''; $bindVars = []; if( !empty( $pParamHash['content_id'] ) ) { $bindVars[] = $pParamHash['content_id']; } elseif( $this->isValid() ) { $bindVars[] = $this->mContentId; } if( @$this->verifyId( $pParamHash['user_id'] ) ) { $whereSql .= " AND lc.`user_id` = ? "; $bindVars[] = $pParamHash['user_id']; } if( !empty( $pParamHash['group_id'] ) ) { $whereSql .= " AND lcperm.`group_id` = ? "; $bindVars[] = $pParamHash['group_id']; } if( !empty( $pParamHash['role_id'] ) ) { $whereSql .= " AND lcperm.`role_id` = ? "; $bindVars[] = $pParamHash['role_id']; } $permWhereSql = ''; $this->getContentPermissionsSql( $pParamHash['perm_name'], $selectSql, $joinSql, $permWhereSql, $bindVars ); if( !empty( $whereSql ) ) { $whereSql = preg_replace( '/^[\s]*AND/', ' ', $whereSql ); } $query = "SELECT COUNT(*) FROM `".BIT_DB_PREFIX."liberty_content` lc $joinSql WHERE lc.`content_id`=? AND ( $whereSql $permWhereSql ) "; $ret = $this->mDb->getOne( $query, $bindVars ); } return !empty( $ret ); } /** * Load all permissions assigned to a given object. * This function is mainly used to fetch a list of custom permissions of a given content item. * * @access public */ public function getContentPermissionsList() { global $gBitUser; $ret = false; if( $this->isValid() ) { $query = " SELECT lcperm.`perm_name`, lcperm.`is_revoked`, ur.`role_id`, ur.`role_name`, up.`perm_desc` FROM `".BIT_DB_PREFIX."liberty_content_permissions` lcperm INNER JOIN `".BIT_DB_PREFIX."users_roles` ur ON( lcperm.`role_id`=ur.`role_id` ) LEFT OUTER JOIN `".BIT_DB_PREFIX."users_permissions` up ON( up.`perm_name`=lcperm.`perm_name` ) WHERE lcperm.`content_id` = ?"; $perms = $this->mDb->getAll( $query, [ $this->mContentId ]); foreach( $perms as $perm ) { $ret[$perm['role_id']][$perm['perm_name']] = $perm; } } return $ret; } /** * Get a list of content with permissions * * @return array */ public static function getContentWithPermissionsList() { global $gBitSystem; $ret = []; $query = " SELECT lcperm.`perm_name`, lc.`title`, lc.`content_id`, lc.`content_type_guid`, lcperm.`is_revoked`, ur.`role_id`, ur.`role_name`, up.`perm_desc` FROM `".BIT_DB_PREFIX."liberty_content_permissions` lcperm INNER JOIN `".BIT_DB_PREFIX."users_roles` ur ON( lcperm.`role_id`=ur.`role_id` ) INNER JOIN `".BIT_DB_PREFIX."liberty_content` lc ON( lcperm.`content_id`=lc.`content_id` ) LEFT OUTER JOIN `".BIT_DB_PREFIX."users_permissions` up ON( up.`perm_name`=lcperm.`perm_name` ) ORDER BY ".$gBitSystem->mDb->convertSortmode( 'content_type_guid_asc' ).", ".$gBitSystem->mDb->convertSortmode( 'title_asc' ); $perms = $gBitSystem->mDb->getAll( $query ); foreach( $perms as $perm ) { $ret[$perm['content_type_guid']][$perm['content_id']][] = $perm; } return $ret; } /** * Expunge Content Permissions * * @access public * @return bool true on success, false on failure - mErrors will contain reason for failure */ public function expungeContentPermissions() { $ret = false; if( $this->isValid() ) { $query = "DELETE FROM `".BIT_DB_PREFIX."liberty_content_permissions` WHERE `content_id` = ?"; $ret = $this->mDb->query( $query, [ $this->mContentId ]); } return $ret; } /** * Function that determines if this content specified permission for the current gBitUser, and will throw a fatal error if not. * * @param string Name of the permission to check * @param string Message if permission denigned */ public function verifyUserPermission( $pPermName, $pFatalMessage = null ) { $ret = true; if( $this->isValid() && !$this->hasUserPermission( $pPermName ) ) { global $gBitSystem; $gBitSystem->fatalPermission( $pPermName, $pFatalMessage ); } return $ret; } /** * public function that determines if this content specified permission for the current gBitUser. * Assigned content perms override the indvidual global perms, so the result is the union of the global permission set + overridden individual content perms * * @param string Name of the permission to check * @param string Check access control service if available * @param string return default user permission setting when no content perms are set * @return bool true if user has permission to access file */ function hasUserPermission( $pPermName, $pVerifyAccessControl=true ) { global $gBitUser; $ret = false; if( !$this->isValid() ) { // return default user permission setting when no content is loaded $ret = $gBitUser->hasPermission( $pPermName ); } elseif( !$gBitUser->isRegistered() || !( $ret = $this->isOwner() || $ret = $gBitUser->isAdmin() )) { if( $gBitUser->isAdmin() || $gBitUser->hasPermission( $this->mAdminContentPerm )) { $ret = true; } else { if( $pVerifyAccessControl ) { $this->verifyAccessControl(); } $checkPerms = $this->getUserPermissions(); if ( !empty( $checkPerms ) ) { // Do they have the admin permission or the one we want? if ( !empty( $checkPerms[$this->mAdminContentPerm] ) ) { $ret = true; } elseif ( !empty( $checkPerms[$pPermName] ) ) { $ret = true; } } else { // return default user permission setting when no content perms are set $ret = $gBitUser->hasPermission( $pPermName ); } } } return $ret; } /** * Determine if current user has the ability to administer this type of content * * @return bool True if user has this type of content administration permission */ public function hasAdminPermission( $pVerifyAccessControl=true ) { return $this->hasUserPermission( $this->mAdminContentPerm, $pVerifyAccessControl ) ; } // === verifyAdminPermission /** * This code was duplicated _EVERYWHERE_ so here is an easy template to cut that down. * It will verify if a given user has a given $permission and if not, it will display the error template and die() * @param $pVerifyAccessControl check access control service if available * @return bool true if permitted, method will fatal out if not * @access public */ function verifyAdminPermission( $pVerifyAccessControl=true ) { global $gBitSystem; if( $this->hasAdminPermission( $pVerifyAccessControl ) ) { return true; } $gBitSystem->fatalPermission( $this->mAdminContentPerm ); return false; } /** * Determine if current user has the ability to delete/expunge this type of content * * @return bool True if user has this type of content expunge permission */ public function hasExpungePermission( $pVerifyAccessControl=true ) { return $this->hasUserPermission( $this->mExpungeContentPerm, $pVerifyAccessControl ); } // === verifyExpungePermission /** * It will verify if a given user has a given $permission and if not, it will display the error template and die() * @param $pVerifyAccessControl check access control service if available * @return bool true if permitted, method will fatal out if not */ public function verifyExpungePermission( $pVerifyAccessControl=true ) { global $gBitSystem; if( $this->hasExpungePermission( $pVerifyAccessControl ) ) { return true; } $gBitSystem->fatalPermission( $this->mExpungeContentPerm ); return false; } /** * Determine if current user has the ability to edit this type of content * * @return bool True if user has this type of content administration permission */ public function hasUpdatePermission( $pVerifyAccessControl=true ) { return $this->hasUserPermission( $this->mUpdateContentPerm, $pVerifyAccessControl ); } /** * Deprecated, use hasUpdatePermission * * @return bool True if user has this type of content administration permission */ function hasEditPermission( $pVerifyAccessControl=true, $pCheckGlobalPerm=true ) { KernelTools::deprecated( "LibertyContent::hasEditPermission has been replaced with LibertyContent::hasUpdatePermission and pCheckGlobal has been change to always be the case" ); return $this->hasUpdatePermission( $pVerifyAccessControl ); } // === verifyUpdatePermission /** * This code was duplicated _EVERYWHERE_ so here is an easy template to cut that down. * It will verify if a given user has a given $permission and if not, it will display the error template and die() * @param $pVerifyAccessControl check access control service if available * @return bool true if permitted, method will fatal out if not * @access public */ public function verifyUpdatePermission( $pVerifyAccessControl=true ) { global $gBitSystem; if( $this->hasUpdatePermission( $pVerifyAccessControl ) ) { return true; } $gBitSystem->fatalPermission( $this->mUpdateContentPerm ); return false; } // === verifyEditPermission /** * Deprecated, use verifyUpdatePermission */ public function verifyEditPermission( $pVerifyAccessControl=true, $pCheckGlobalPerm=true ) { KernelTools::deprecated( "LibertyContent::verifyEditPermission has been replaced with LibertyContent::verifyUpdatePermission and pCheckGlobal has been change to always be the case" ); $this->verifyUpdatePermission( $pVerifyAccessControl ); } /** * Determine if current user has the ability to craete this type of content * * @return bool True if user has this type of content administration permission */ public function hasCreatePermission( $pVerifyAccessControl=true ) { return $this->hasUserPermission( $this->mCreateContentPerm, $pVerifyAccessControl ); } // === verifyCreatePermission /** * Determine if current user has the ability to create this type of content * Note this will always return falseif the content isValid * * @return bool True if user has this type of content administration permission **/ public function verifyCreatePermission( $pVerifyAccessControl=true ) { global $gBitSystem; if( !$this->isValid() && $this->hasCreatePermission( $pVerifyAccessControl ) ) { return true; } $gBitSystem->fatalPermission( $this->mCreateContentPerm ); return false; } /** * Determine if current user has the ability to view this type of content * Note that this will always return true if you haven't set the mViewContentPerm in your class * * @return bool True if user has this type of content administration permission */ public function hasViewPermission( $pVerifyAccessControl=true ) { return $this->hasUpdatePermission( $pVerifyAccessControl ) || empty( $this->mViewContentPerm ) || $this->hasUserPermission( $this->mViewContentPerm, $pVerifyAccessControl ); } // === verifyViewPermission /** * This code was duplicated _EVERYWHERE_ so here is an easy template to cut that down. * It will verify if a given user has a given $permission and if not, it will display the error template and die() * @param $pVerifyAccessControl check access control service if available * @return bool true if permitted, method will fatal out if not * @access public */ public function verifyViewPermission( $pVerifyAccessControl=true ) { global $gBitSystem; if( $this->hasViewPermission( $pVerifyAccessControl ) ) { return true; } $gBitSystem->fatalPermission( $this->mViewContentPerm ); return false; } /** * Determine if current user has the ability to post comments to this type of content * * @return bool True if user has this type of content administration permission */ public function hasPostCommentsPermission( $pVerifyAccessControl=true ) { return $this->hasUserPermission( 'p_liberty_post_comments', $pVerifyAccessControl ); } // === verifyPostCommentsPermission /** * It will verify if a given user has a given $permission and if not, it will display the error template and die() * @param $pVerifyAccessControl check access control service if available * @return bool true if permitted, method will fatal out if not * @access public */ public function verifyPostCommentsPermission( $pVerifyAccessControl=true ) { global $gBitSystem; // @TODO Missing function /* if( $this->hasPostCommentPermission( $pVerifyAccessControl ) ) { return true; } else { $gBitSystem->fatalPermission( 'p_liberty_post_comments' ); } */ return false; } /** * Get specific permissions for the specified user for this content * * @return array of all permissions for the current user joined with perms * for the current content. This should handle cases where * non-default permissions is assigned, default permission is * removed, and duplicate default permissions where one team's perm * is revoked, but another is still permitted. If the permission is * revoked, is_revoked will be set to 'y' */ public function getUserPermissions() { global $gBitUser; $userId = $gBitUser->mUserId; // Prevent null entires when creating database if( !is_numeric( $userId ) ) $userId = 0; if( !is_numeric( $this->mContentId ) ) $this->mContentId = 0; if( !isset( $this->mUserContentPerms )) { // get the default permissions for specified user $query = " SELECT urp.`perm_name` as `hash_key`, 1 as `role_perm`, urp.`perm_name`, urp.`perm_value`, urp.`role_id` FROM `".BIT_DB_PREFIX."users_roles_map` urm LEFT JOIN `".BIT_DB_PREFIX."users_role_permissions` urp ON(urm.`role_id`=urp.`role_id`) LEFT JOIN `".BIT_DB_PREFIX."liberty_content_permissions` lcp ON(lcp.`role_id`=urm.`role_id` AND lcp.`content_id`=? AND urp.`perm_name`=lcp.`perm_name`) WHERE (urm.`user_id`=? OR urm.`user_id`=?) AND lcp.`perm_name` IS null"; if( !$defaultPerms = $this->mDb->getAssoc( $query, [ $this->mContentId, $userId, ANONYMOUS_USER_ID ] ) ) { $defaultPerms = []; } $query = " SELECT lcp.`perm_name` AS `hash_key`, lcp.* FROM `".BIT_DB_PREFIX."liberty_content_permissions` lcp INNER JOIN `".BIT_DB_PREFIX."users_roles_map` urm ON(lcp.role_id=urm.role_id) LEFT JOIN `".BIT_DB_PREFIX."users_role_permissions` urp ON(urm.role_id=urp.role_id AND urp.role_id!=lcp.role_id AND urp.perm_name=lcp.perm_name) WHERE lcp.content_id=? AND (urm.user_id=? OR urm.user_id=?) AND lcp.is_revoked IS null"; if( !$nonDefaultPerms = $this->mDb->getAssoc( $query, [ $this->mContentId, $userId, ANONYMOUS_USER_ID ] ) ) { $nonDefaultPerms = []; } $this->mUserContentPerms = array_merge( $defaultPerms, $nonDefaultPerms ); $this->invokeServices( 'content_user_perms_function' ); } return $this->mUserContentPerms; } /** * Store a permission for the object that has been loaded in the permission database * * Any old copy of the permission is deleted prior to loading the new copy * @param integer $pTeamId Group Identifier * @param string Name of the permission * @param integer Content Itentifier * @return bool true ( will not currently report a failure ) */ public function storePermission( $pTeamId, $pPermName, $pIsRevoked=false, $pContentId=null ){ $ret = false; $pContentId = $pContentId == null?$this->mContentId:$pContentId; if( BitBase::verifyId( $pTeamId ) && !empty( $pPermName ) && BitBase::verifyId( $pContentId ) ) { $this->removePermission( $pTeamId, $pPermName, $pContentId ); $storeHash = [ 'perm_name' => $pPermName, 'content_id' => $pContentId, ]; $storeHash['role_id'] = $pTeamId; // check to see if this is an exclusion if( $pIsRevoked ) { $storeHash['is_revoked'] = 'y'; } $ret = $this->mDb->associateInsert( BIT_DB_PREFIX."liberty_content_permissions", $storeHash ); } return $ret; } /** * Remove a permission to access the content * * @param integer Group Identifier * @param string Name of the permission * @return bool true ( will not currently report a failure ) */ public function removePermission( $pTeamId, $pPermName, $pContentId=null ) { $pContentId = $pContentId == null?$this->mContentId:$pContentId; if( BitBase::verifyId( $pTeamId ) && !empty( $pPermName ) && BitBase::verifyId( $pContentId ) ) { $query = " DELETE FROM `".BIT_DB_PREFIX."liberty_content_permissions` WHERE `role_id` = ? and `content_id` = ? and `perm_name` = ?"; $bindVars = [ $pTeamId, $pContentId, $pPermName ]; $result = $this->mDb->query( $query, $bindVars ); } return true; } /** * Check to see if this permission is already in the global permissions table. * * @param array $pTeamId * @param array $pPermName * @access public * @return bool true if present, false if not */ public function isExcludedPermission( $pTeamId, $pPermName ) { if( BitBase::verifyId( $pTeamId ) && !empty( $pPermName )) { $query = "SELECT `perm_name` FROM `".BIT_DB_PREFIX."users_role_permissions` WHERE `role_id` = ? AND `perm_name` = ?"; return $this->mDb->getOne( $query, [ $pTeamId, $pPermName ] ) == $pPermName; } return false; } // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= Preferences Functions /** * Returns the content preferences value for the passed in key. * * @param string Hash key for the mPrefs value * @param string Default value to return if the preference is empty * @param int Optional content_id for arbitrary content preference */ public static function getContentPreference( $pContentId, $pPrefName, $pPrefDefault=null ) { global $gBitDb; $ret = null; if( parent::verifyId( $pContentId ) && !empty( $pPrefName )) { // Get a user preference for an arbitrary user $sql = "SELECT `pref_value` FROM `".BIT_DB_PREFIX."liberty_content_prefs` WHERE `content_id`=? AND `pref_name`=?"; if( !$ret = $gBitDb->getOne( $sql, [ $pContentId, $pPrefName ] ) ) { $ret = $pPrefDefault; } } return $ret; } /** * Returns the content preferences value for the passed in key. * * @param string Hash key for the mPrefs value * @param string Default value to return if the preference is empty * @param int Optional content_id for arbitrary content preference */ public function getPreference( $pPrefName, $pPrefDefault=null ) { global $gBitDb; $ret = null; if( $this->isValid() ) { if( is_null( $this->mPrefs ) ) { $this->loadPreferences(); } $ret = isset( $this->mPrefs ) && isset( $this->mPrefs[$pPrefName] ) ? $this->mPrefs[$pPrefName] : $pPrefDefault; } return $ret; } /** * loadPreferences of the currently loaded object or pass in to get preferences of a specific content_id * * @param numeric $pContentId content_id of the item we want the prefs from (optional) * @access public * @return array of preferences if $pContentId is set or pass preferences on to $this->mPrefs */ public function loadPreferences( $pContentId = null ) { global $gBitSystem; if( BitBase::verifyId( $pContentId )) { return $gBitSystem->mDb->getAssoc( "SELECT `pref_name`, `pref_value` FROM `".BIT_DB_PREFIX."liberty_content_prefs` WHERE `content_id`=?", [ $pContentId ]); } elseif( $this->isValid() ) { // If no results, getAssoc will return an empty array (ie not a true null value) so getPreference can tell we have attempted a load $this->mPrefs = @$this->mDb->getAssoc( "SELECT `pref_name`, `pref_value` FROM `".BIT_DB_PREFIX."liberty_content_prefs` WHERE `content_id`=?", [ $this->mContentId ]); } return []; } /** * Set a hash value in the mPrefs hash. This does *NOT* store the value in the database. It does no checking for existing or duplicate values. the main point of this function is to limit direct accessing of the mPrefs hash. I will probably make mPrefs private one day. * * @param string Hash key for the mPrefs value * @param string Value for the mPrefs hash key */ public function setPreference( $pPrefName, $pPrefValue ) { $this->mPrefs[$pPrefName] = $pPrefValue; } /** * Saves a preference to the liberty_content_prefs database table with the given pref name and value. If the value is null, the existing value will be delete and the value will not be saved. However, a zero will be stored. This will update the mPrefs hash. * * @param string Hash key for the mPrefs value * @param string Value for the mPrefs hash key */ public function storePreference( $pPrefName, $pPrefValue = null ) { $ret = false; if( LibertyContent::isValid() ) { $this->StartTrans(); $query = "DELETE FROM `".BIT_DB_PREFIX."liberty_content_prefs` WHERE `content_id`=? AND `pref_name`=?"; $bindvars = [ $this->mContentId, $pPrefName ]; $result = $this->mDb->query($query, $bindvars); if( !is_null( $pPrefValue )) { $query = "INSERT INTO `".BIT_DB_PREFIX."liberty_content_prefs` (`content_id`,`pref_name`,`pref_value`) VALUES(?, ?, ?)"; $bindvars[] = substr( $pPrefValue, 0, 510 ); $result = $this->mDb->query( $query, $bindvars ); $this->mPrefs[$pPrefName] = $pPrefValue; } $this->mPrefs[$pPrefName] = $pPrefValue; $this->CompleteTrans(); $this->clearFromCache(); } return $ret; } /** * Register the content type for reference * * @param string Content Type GUID * @param array Array of content type data * Populates the mType array with the following entries * string content_type_guid * string */ public function registerContentType( $pContentGuid, $pTypeParams ) { global $gLibertySystem; $gLibertySystem->registerContentType( $pContentGuid, $pTypeParams ); $this->mType = $pTypeParams; $this->mContentTypeGuid = $pContentGuid; $pkg = $pTypeParams['handler_package'] ?? ''; $this->mPackageGuid = ( $pkg && $pkg !== $pContentGuid ) ? $pkg : ''; $this->mXrefType = null; } /** * Increment the content item hit flag by 1 * * @return bool true ( will not currently report a failure ) */ public function addHit() { global $gBitUser,$gBitSystem; if( empty( $_REQUEST['post_comment_submit'] ) && empty( $_REQUEST['post_comment_request'] ) ) { if( BitBase::verifyId( $this->mContentId ) && (( $gBitUser->isRegistered() && !$this->isOwner() ) || ( $gBitUser->getField( 'user_id' ) == ANONYMOUS_USER_ID )) && ( $gBitSystem->isFeatureActive( 'users_count_admin_pageviews' ) || !$gBitUser->isAdmin() ) ) { $now = $gBitSystem->getUTCTime(); try { $this->mDb->query( "UPDATE `".BIT_DB_PREFIX."liberty_content_hits` SET `hits`=`hits`+1, `last_hit`=? WHERE `content_id`=?", [ $now, $this->mContentId ] ); if( $this->mDb->Affected_Rows() == 0 ) { $this->mDb->query( "INSERT INTO `".BIT_DB_PREFIX."liberty_content_hits` (`hits`,`last_hit`,`content_id`) VALUES (1,?,?)", [ $now, $this->mContentId ] ); } } catch( \Exception $e ) { // Race: concurrent request inserted between our UPDATE (0 rows) and INSERT — harmless } } } return true; } /** * Set Hits and Last Hit * * @return bool true ( will not currently report a failure ) */ public function setHits($pHits, $pLastHit=0) { if( $this->mContentId && !empty($pHits) ) { $query = "UPDATE `".BIT_DB_PREFIX."liberty_content_hits` SET `hits`= ?, `last_hit`= ? WHERE `content_id` = ?"; $result = $this->mDb->query( $query, [ $pHits, $pLastHit, $this->mContentId ] ); $affected_rows = $this->mDb->Affected_Rows(); if( !$affected_rows ) { $query = "INSERT INTO `".BIT_DB_PREFIX."liberty_content_hits` ( `hits`, `last_hit`, `content_id` ) VALUES (?,?,?)"; $result = $this->mDb->query( $query, [ $pHits, $pLastHit, $this->mContentId ] ); } } return true; } /** * Get Hits and Last Hit * * @return bool true ( will not currently report a failure ) */ public function getHits() { if( $this->mContentId ) { $query = "SELECT `hits`,`last_hit` FROM `".BIT_DB_PREFIX."liberty_content_hits` where `content_id` = ?"; $row = $this->mDb->getRow( $query, [ $this->mContentId ] ); if ( !empty($row) ) { $this->mInfo['hits'] = $row['hits']; $this->mInfo['last_hit'] = $row['last_hit']; } } return true; } /** * Create the generic title for a content item * * This can be overwriten by extended classes to provide an appropriate title string * @param array pHash type hash of data to be used to provide base data * @return string Descriptive title for the page */ public static function getTitleFromHash( &$pHash, $pDefault=true ) { $ret = ''; if( !empty( $pHash['title'] ) ) { $ret = $pHash['title']; } elseif( $pDefault && !empty( $pHash['content_name'] ) ) { $ret = $pHash['content_name']; } return $ret; } /** * Create the generic title for a content item * * This will normally be overwriten by extended classes to provide * an appropriate title string * @return string Descriptive title for the page */ public function getTitle() { $ret = null; if( $this->isValid() ) { $ret = static::getTitleFromHash( $this->mInfo ); } return $ret; } /** * Get the time this object was created * * @return int Unix epoch of time object was created */ public function getTimeCreated() { $ret = null; if( $this->isValid() ) { $ret = $this->getField( 'created' ); } return $ret; } /** * Get the time this object was last modified * * @return int Unix epoch of time object was last modified */ public function getTimeModified() { $ret = null; if( $this->isValid() ) { $ret = $this->getField( 'last_modified' ); } return $ret; } /** * Attempt to create a brief description of this object, most useful for * * @return string list of aliases */ public function generateDescription() { $ret = ''; if( $this->isValid() ) { if( $this->getField('summary') ) { $ret = $this->getField('summary'); } elseif( $this->getField('data') ) { $text = trim( preg_replace('/\s+/', ' ', strip_tags( $this->getParsedData() ?? '' ) ) ); // 250 to 300 is max description $ret = substr( $text, 0, 250 ); } } return $ret; } /** * Attempt to create a collection of relevant words about this object, most useful for * * @return array list of aliases */ public function generateKeywords() { $ret = []; if( $this->isValid() ) { } return $ret; } /** * Get array of aliases for this content object * * @return array list of aliases */ public function getAliases( $pUpperCase = false ) { global $gBitSystem; $ret = []; if( $this->isValid() ) { $selectColumn = $pUpperCase ? $gBitSystem->mDb->getCaseLessColumn('alias_title') : '`alias_title`'; $ret = $this->mDb->getCol( "SELECT ".$selectColumn." FROM `".BIT_DB_PREFIX."liberty_aliases` lal INNER JOIN `".BIT_DB_PREFIX."liberty_content` lc ON(lal.`content_id`=lc.`content_id`) WHERE lal.`content_id`=? ", [ $this->mContentId ], BIT_QUERY_CACHE_TIME ); } return $ret; } /** * Access a content item type GUID * * @return string content_type_guid for the content */ public function getContentType() { $ret = null; if( isset( $this->mInfo['content_type_guid'] ) ) { $ret = $this->mInfo['content_type_guid']; } elseif( $this->mContentTypeGuid ) { // for unloaded classes $ret = $this->mContentTypeGuid; } elseif( $this->mType['content_type_guid'] ) { // unloaded content might have this $ret = $this->mType['content_type_guid']; } return $ret; } /** * Get the display name of the content type * @param boolean $pPlural true will return the plural form of the content type display name * @return string the display name of the content type */ public function getContentTypeName( $pPlural=false ){ global $gLibertySystem; return $gLibertySystem->getContentTypeName( $this->getContentType(), $pPlural ); } /** * Access a content item content_id * * @return string content_type_guid for the object */ public function getContentId() { $ret = null; if( isset( $this->mContentId ) ) { $ret = $this->mContentId; } return $ret; } /** * Return content type description for this content object. * * @return string content_type_guid description for the object */ public function getContentDescription() { KernelTools::deprecated( 'You are calling the deprecated method getContentDescription, use getContentTypeName( $pPlural )' ); return $this->getContentTypeName(); } /** * returns a path to the template type requested * this is intended for package override. while not a requirement please use a naming convention of center__.tpl for new tpls * * @param string $pAction the type of template. common types are view and list */ public function getViewTemplate( $pAction ) { $ret = null; switch ( $pAction ){ case "view": case "list": $ret = "bitpackage:liberty/center_".$pAction."_generic.tpl"; break; } return $ret; } /** * Pure virtual function that returns the include file that should render a page of content of this type * @return string the fully specified path to file to be included */ public function getRenderFile() { return LIBERTY_PKG_INCLUDE_PATH.'display_content_inc.php'; } public function getDisplayLink( $pParamHash, $pLinkText=null, $pAnchor=null ) { return self::getDisplayLinkFromHash( $this->mInfo, $pLinkText, $pAnchor ); } /** * Pure virtual function that returns link to display a piece of content * * @param string $pLinkText Text for the link unless overriden by object title * @param array $pParamHash different possibilities depending on derived class * @param string $pAnchor anchor string e.g.: #comment_123 * @return string Formated html the link to display the page. */ public static function getDisplayLinkFromHash( &$pParamHash, $pLinkText=null, $pAnchor=null ) { global $gBitSmarty, $gBitweaverExtension; $ret = ''; if( empty( $pLinkText )) { if( !empty( $pParamHash['title'] )) { $pLinkText = $pParamHash['title']; } elseif( !empty( $pParamHash['content_name'] ) ) { $pLinkText = "[ ".$pParamHash['content_name']." ]"; } } if( empty( $pLinkText )) { $pLinkText = "[ ".KernelTools::tra( "No Title" )." ]"; } // we add some more info to the title of the link $linkTitle = !empty( $pParamHash['created'] ) ? KernelTools::tra( 'Created' ).': ' . $gBitweaverExtension->smarty_modifier_bit_short_date( $pParamHash['created'] ) : $pLinkText; // finally we are ready to create the full link if( !empty( $pParamHash['content_id'] )) { $ret = ''.htmlspecialchars( $pLinkText ).''; } return $ret; } /** * Not-so-pure virtual function that returns fully qualified URI to a piece of content * @param string Text for DisplayLink function * @param array different possibilities depending on derived class * @return string Formated URL address to display the page. */ public function getDisplayUri() { if( $this->isValid() ) { return BIT_ROOT_URI.substr( static::getDisplayUrlFromHash( $this->mInfo ), strlen( BIT_ROOT_URL ) ); } return ''; } /** * Not-so-pure virtual function that returns fully qualified URI to a piece of content * @param string Text for DisplayLink function * @param array different possibilities depending on derived class * @return string Formated URL address to display the page. */ public static function getDisplayUriFromHash( &$pParamHash ) { return BIT_ROOT_URI.substr( static::getDisplayUrlFromHash( $pParamHash ), strlen( BIT_ROOT_URL ) ); } /** * Not-so-pure virtual function that returns Request_URI to a piece of content * @param array $pParamHash a hash of params to add to the url * @return string Formated URL address to display the page. */ public static function getDisplayUrlFromHash( &$pParamHash ) { $ret = null; if( BitBase::verifyId( $pParamHash['content_id'] ) ) { $ret = BIT_ROOT_URL.'index.php?content_id='.$pParamHash['content_id']; } return $ret; } /** * Returns Request URL to a piece of content */ public function getDisplayUrl() { $ret = null; if( !empty( $this ) && $this->isValid() ) { $ret = static::getDisplayUrlFromHash( $this->mInfo ); } return $ret; } /** * Returns the create/edit url to a piece of content * @param number $pContentId a valid content id * @param array $pMixed a hash of params to add to the url */ /** * Hook for packages to enrich a single xref row after it has been loaded. * * Called by edit_xref.php when displaying a single xref for editing. * Override in a package class to add computed fields (e.g. contact title from * an xref content_id, component pack size) directly into the row array before * it reaches the template. * * @param array &$pXrefInfo the raw xref row from liberty_xref (mutate in place) */ public function enrichXrefDisplay( array &$pXrefInfo ): void {} /** * Resolve the Smarty group template for an xref group. * * Template resolution order (first match wins): * 1. `/templates//view_xref_