diff options
| author | Lester Caine <lester@lsces.co.uk> | 2026-06-06 15:03:30 +0100 |
|---|---|---|
| committer | Lester Caine <lester@lsces.co.uk> | 2026-06-06 15:03:30 +0100 |
| commit | be5df3df206bd49417ef0d5a68033e22de747e8b (patch) | |
| tree | e68d42acb150ca2267f187491ecc522a1b9dcb08 /includes | |
| parent | 87ffc50fc40575ef2ade7aac80b274c2e2c542e0 (diff) | |
| download | liberty-be5df3df206bd49417ef0d5a68033e22de747e8b.tar.gz liberty-be5df3df206bd49417ef0d5a68033e22de747e8b.tar.bz2 liberty-be5df3df206bd49417ef0d5a68033e22de747e8b.zip | |
xref: migrate query methods to LibertyXrefType; docblocks throughout
LibertyXrefType gains five runtime statics (getDisplayGroups,
getTypeMarkers, getAvailableItems, getTemplateFormats,
getContentTypeMarkers) — role-filtered, content-type-scoped.
LibertyContent xref query methods become one-line delegates.
LibertyXrefInfo added as new class (was missing from repo).
Docblocks added to LibertyContent class, LibertyXref, LibertyXrefGroup,
LibertyXrefInfo, LibertyXrefType.
list_xref.tpl: remove dead legacy $source path; new path only.
loadXrefList() removed from LibertyContent (stock fully migrated).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'includes')
| -rwxr-xr-x | includes/classes/LibertyContent.php | 312 | ||||
| -rw-r--r-- | includes/classes/LibertyXref.php | 87 | ||||
| -rw-r--r-- | includes/classes/LibertyXrefGroup.php | 54 | ||||
| -rw-r--r-- | includes/classes/LibertyXrefInfo.php | 87 | ||||
| -rw-r--r-- | includes/classes/LibertyXrefType.php | 260 |
5 files changed, 648 insertions, 152 deletions
diff --git a/includes/classes/LibertyContent.php b/includes/classes/LibertyContent.php index 3798639..13d0f4d 100755 --- a/includes/classes/LibertyContent.php +++ b/includes/classes/LibertyContent.php @@ -50,8 +50,66 @@ if( !defined( 'BIT_CONTENT_DEFAULT_STATUS' ) ) { define( 'LIBERTY_SPLIT_REGEX', "!\.[3]split\.[3][\t ]*\n?!" ); /** - * Virtual base class (as much as one can have such things in PHP) for all - * derived tikiwiki classes that require database access. + * 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 LibertyXrefInfo) 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 */ @@ -85,6 +143,9 @@ class LibertyContent extends LibertyBase implements BitCacheable { */ public $mType; + /** Populated by loadXrefInfo() — LibertyXrefInfo instance for this content item */ + public ?LibertyXrefInfo $mXrefInfo = null; + /** *Permissions hash specific to the user accessing this LibertyContent object * @public @@ -2155,8 +2216,35 @@ class LibertyContent extends LibertyBase implements BitCacheable { * @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. `<package>/templates/<content_type_guid>/view_xref_<template>_group.tpl` + * 2. `<package>/templates/view_xref_<template>_group.tpl` + * 3. `bitpackage:liberty/list_xref.tpl` (generic fallback) + * + * The package is taken from `$this->mType['handler_package']`, defaulting to + * 'liberty'. Pass the value of `$xrefGroup->mTemplate` as `$pTemplate`. + * A null or empty template skips straight to the fallback. + * + * Called from templates as: `$gContent->getXrefListTemplate($xrefGroup->mTemplate)` + * + * @param string|null $pTemplate liberty_xref_group.template value + * @return string bitpackage: path to the group template + */ public function getXrefListTemplate( ?string $pTemplate = null ): string { if( $pTemplate ) { $package = $this->mType['handler_package'] ?? 'liberty'; @@ -2175,6 +2263,20 @@ class LibertyContent extends LibertyBase implements BitCacheable { return 'bitpackage:liberty/list_xref.tpl'; } + /** + * Resolve the Smarty item template for displaying a single xref row (view path). + * + * Template resolution order (first match wins): + * 1. `<package>/templates/<content_type_guid>/view_xref_<template>_item.tpl` + * 2. `<package>/templates/view_xref_<template>_item.tpl` + * 3. `bitpackage:liberty/view_xref_<template>_item.tpl` + * 4. `bitpackage:liberty/view_xref_text_item.tpl` (hardcoded fallback) + * + * Defaults to 'text' if `$pTemplate` is null/empty. + * + * @param string|null $pTemplate liberty_xref_item.template value + * @return string bitpackage: path to the item view template + */ public function getXrefRecordTemplate( ?string $pTemplate = null ): string { $pTemplate = $pTemplate ?: 'text'; $package = $this->mType['handler_package'] ?? 'liberty'; @@ -2195,6 +2297,20 @@ class LibertyContent extends LibertyBase implements BitCacheable { : 'bitpackage:liberty/view_xref_text_item.tpl'; } + /** + * Resolve the Smarty item template for editing a single xref row (edit path). + * + * Template resolution order (first match wins): + * 1. `<package>/templates/<content_type_guid>/edit_xref_<template>_item.tpl` + * 2. `<package>/templates/edit_xref_<template>_item.tpl` + * 3. `bitpackage:liberty/edit_xref_<template>_item.tpl` + * 4. `bitpackage:liberty/edit_xref.tpl` (hardcoded fallback) + * + * Defaults to 'text' if `$pTemplate` is null/empty. + * + * @param string|null $pTemplate liberty_xref_item.template value + * @return string bitpackage: path to the item edit template + */ public function getXrefEditTemplate( ?string $pTemplate = null ): string { $pTemplate = $pTemplate ?: 'text'; $package = $this->mType['handler_package'] ?? 'liberty'; @@ -3794,167 +3910,65 @@ class LibertyContent extends LibertyBase implements BitCacheable { } // ========================================================================= - // Xref methods — generic for any LibertyContent subclass. - // Queries scope to $this->mContentTypeGuid so each package sees only its - // own liberty_xref_group / liberty_xref_item / liberty_xref rows. + // Xref methods — delegates to LibertyXrefType (query logic) and + // LibertyXref / LibertyXrefInfo (data logic). // ========================================================================= - /** - * Returns xref type groups (sort_order > 0) for this content type. - * Each row includes xref_type aliased as 'source' for template use. - */ + /** @see LibertyXrefType::getDisplayGroups() */ public function getXrefGroupList(): array { - global $gBitUser; - $roles = array_keys( $gBitUser->mRoles ?? [] ) ?: [-1]; - $bindVars = array_merge( $roles, [ $gBitUser->mUserId ] ); - $query = "SELECT g.*, g.`x_group` AS source FROM `".BIT_DB_PREFIX."liberty_xref_group` g - LEFT OUTER JOIN `".BIT_DB_PREFIX."users_roles_map` purm ON ( purm.`user_id`=".$gBitUser->mUserId." ) AND ( purm.`role_id`=g.`role_id` ) - WHERE g.`content_type_guid` = '".$this->mContentTypeGuid."' AND g.`sort_order` > 0 AND (g.`role_id` IN(". implode(',', array_fill(0, count($roles), '?')) ." ) OR purm.`user_id`=?) - ORDER BY g.`sort_order`"; - $result = $this->mDb->query( $query, $bindVars ); - $ret = []; - while ( $res = $result->fetchRow() ) { - $ret[] = $res; - } - return $ret; + return LibertyXrefType::getDisplayGroups( $this->mContentTypeGuid ); } - /** - * Returns xref sources at sort_order=0 (the top-level type/category markers). - */ + /** @see LibertyXrefType::getTypeMarkers() */ public function getXrefSourceList(): array { - global $gBitUser; - $roles = array_keys( $gBitUser->mRoles ?? [] ) ?: [-1]; - $bindVars = array_merge( $roles, [ $gBitUser->mUserId ] ); - $query = "SELECT g.`cross_ref_title` AS `type_name`, g.`item` FROM `".BIT_DB_PREFIX."liberty_xref_item` g - JOIN `".BIT_DB_PREFIX."liberty_xref_group` t ON t.`x_group` = g.`x_group` AND t.`content_type_guid` = '".$this->mContentTypeGuid."' - LEFT OUTER JOIN `".BIT_DB_PREFIX."users_roles_map` purm ON ( purm.`user_id`=".$gBitUser->mUserId." ) AND ( purm.`role_id`=g.`role_id` ) - WHERE g.`content_type_guid` = '".$this->mContentTypeGuid."' AND t.`sort_order` = 0 AND (g.`role_id` IN(". implode(',', array_fill(0, count($roles), '?')) ." ) OR purm.`user_id`=?) - ORDER BY g.`item`"; - $result = $this->mDb->query( $query, $bindVars ); - $ret = []; - $cnt = 0; - while ( $res = $result->fetchRow() ) { - $ret[$cnt]['item'] = $res['item']; - $ret[$cnt++]['name'] = trim( $res['type_name'] ); - } - return $ret; + return LibertyXrefType::getTypeMarkers( $this->mContentTypeGuid ); } - /** - * Returns available xref source types, optionally filtered by group (sort_order - * integer) or template name. Used to populate the add-xref type selector. - */ - public function getXrefTypeList( $xrefGroup = 0, $xrefTemplate = NULL ): array { - if ( $xrefTemplate ) { - $query = "SELECT s.`cross_ref_title` AS `type_name`, s.`item`, s.`template` FROM `".BIT_DB_PREFIX."liberty_xref_item` s - WHERE s.`content_type_guid` = '".$this->mContentTypeGuid."' AND s.`template` = '$xrefTemplate' - ORDER BY s.`cross_ref_title`"; - $result = $this->mDb->query( $query, [] ); - } elseif ( $xrefGroup > -1 ) { - $query = "SELECT s.`cross_ref_title` AS `type_name`, s.`item`, s.`template` FROM `".BIT_DB_PREFIX."liberty_xref_item` s - JOIN `".BIT_DB_PREFIX."liberty_xref_group` t ON t.`x_group` = s.`x_group` AND t.`content_type_guid` = '".$this->mContentTypeGuid."' - LEFT JOIN `".BIT_DB_PREFIX."liberty_xref` x ON x.`item` = s.`item` AND x.`content_id` = ? AND ( x.`end_date` IS NULL OR x.`end_date` > CURRENT_TIMESTAMP ) - WHERE s.`content_type_guid` = '".$this->mContentTypeGuid."' AND t.`sort_order` = ? AND ( x.`xref_id` IS NULL OR x.`xorder` > 0 ) - ORDER BY s.`cross_ref_title`"; - $result = $this->mDb->query( $query, [ $this->mContentId, $xrefGroup ] ); - } else { - $query = "SELECT s.`cross_ref_title` AS `type_name`, s.`item`, s.`template` FROM `".BIT_DB_PREFIX."liberty_xref_item` s - JOIN `".BIT_DB_PREFIX."liberty_xref_group` t ON t.`x_group` = s.`x_group` AND t.`content_type_guid` = '".$this->mContentTypeGuid."' - LEFT JOIN `".BIT_DB_PREFIX."liberty_xref` x ON x.`item` = s.`item` AND x.`content_id` = ? AND ( x.`end_date` IS NULL OR x.`end_date` > CURRENT_TIMESTAMP ) - WHERE s.`content_type_guid` = '".$this->mContentTypeGuid."' AND t.`sort_order` > 0 AND ( x.`xref_id` IS NULL OR x.`xorder` > 0 ) - ORDER BY s.`cross_ref_title`"; - $result = $this->mDb->query( $query, [ $this->mContentId ] ); - } - $ret = []; - while ( $res = $result->fetchRow() ) { - $ret['list'][$res['item']] = trim( $res['type_name'] ); - $ret['type'][$res['item']] = trim( $res['template'] ) !== '' ? trim( $res['template'] ) : 'generic'; - } - return $ret; + /** @see LibertyXrefType::getAvailableItems() */ + public function getXrefTypeList( $xrefGroup = 0, $xrefTemplate = null ): array { + return LibertyXrefType::getAvailableItems( $this->mContentTypeGuid, $this->mContentId, $xrefGroup, $xrefTemplate ); } - /** - * Returns distinct xref template format names for this content type. - */ + /** @see LibertyXrefType::getTemplateFormats() */ public function getXrefFormatList(): array { - global $gBitUser; - $roles = array_keys( $gBitUser->mRoles ?? [] ) ?: [-1]; - $bindVars = array_merge( $roles, [ $gBitUser->mUserId ] ); - $query = "SELECT DISTINCT g.`template` FROM `".BIT_DB_PREFIX."liberty_xref_item` g - LEFT OUTER JOIN `".BIT_DB_PREFIX."users_roles_map` purm ON ( purm.`user_id`=".$gBitUser->mUserId." ) AND ( purm.`role_id`=g.`role_id` ) - WHERE g.`content_type_guid` = '".$this->mContentTypeGuid."' AND (g.`role_id` IN(". implode(',', array_fill(0, count($roles), '?')) ." ) OR purm.`user_id`=?) - ORDER BY g.`template`"; - $result = $this->mDb->query( $query, $bindVars ); - $ret = []; - while ( $res = $result->fetchRow() ) { - $ret[] = trim( $res['template'] ) !== '' ? trim( $res['template'] ) : 'generic'; - } - return $ret; + return LibertyXrefType::getTemplateFormats( $this->mContentTypeGuid ); } /** - * Loads xref type markers (sort_order=0 sources, showing which categories apply - * to this content item) into mInfo[$this->mXrefTypeKey]. - * Subclasses set $mXrefTypeKey to control the mInfo key (default: 'xref_types'). + * Load sort_order=0 type markers for this content item into mInfo. + * + * Results land in mInfo[$this->mXrefTypeKey] (default: 'xref_types'). + * Subclasses set $mXrefTypeKey to a package-specific key, e.g. Contact uses + * 'contact_types' so edit.php can detect $isPerson. No-ops if already populated. + * + * @see LibertyXrefType::getContentTypeMarkers() */ public function loadXrefTypeList(): void { - if ( $this->isValid() && empty( $this->mInfo[$this->mXrefTypeKey] ) ) { - global $gBitUser; - $roles = array_keys( $gBitUser->mRoles ?? [] ) ?: [-1]; - $bindVars = array_merge( [ $this->mContentId ], $roles, [ $gBitUser->mUserId ] ); - $sql = "SELECT r.`item`, r.`cross_ref_title`, d.`content_id` - FROM `".BIT_DB_PREFIX."liberty_xref_item` r - JOIN `".BIT_DB_PREFIX."liberty_xref_group` t ON t.`x_group` = r.`x_group` AND t.`content_type_guid` = '".$this->mContentTypeGuid."' - LEFT JOIN `".BIT_DB_PREFIX."liberty_xref` d ON d.`content_id` = ? AND d.`item` = r.`item` - LEFT OUTER JOIN `".BIT_DB_PREFIX."users_roles_map` purm ON ( purm.`user_id`=".$gBitUser->mUserId." ) AND ( purm.`role_id`=r.`role_id` ) - WHERE r.`content_type_guid` = '".$this->mContentTypeGuid."' AND t.`sort_order` = 0 AND (r.`role_id` IN(". implode(',', array_fill(0, count($roles), '?')) ." ) OR purm.`user_id`=?) - ORDER BY r.`item`"; - $result = $this->mDb->query( $sql, $bindVars ); - while ( $res = $result->fetchRow() ) { - $this->mInfo[$this->mXrefTypeKey][] = $res; - } + if( $this->isValid() && empty( $this->mInfo[$this->mXrefTypeKey] ) ) { + $this->mInfo[$this->mXrefTypeKey] = LibertyXrefType::getContentTypeMarkers( $this->mContentTypeGuid, $this->mContentId ); } } /** - * Loads all xref records for this content item into mInfo keyed by type_source - * (the liberty_xref_group.xref_type text key, or 'history' for expired records). + * Populate mXrefInfo with a LibertyXrefInfo instance for this content item. + * Creates one LibertyXrefGroup per display group (sort_order > 0), each holding + * its raw xref data rows. */ - public function loadXrefList(): void { - if ( $this->isValid() && empty( $this->mInfo['xref'] ) ) { - global $gBitUser; - $roles = array_keys( $gBitUser->mRoles ?? [] ) ?: [-1]; - $bindVars = array_merge( [ $this->mDb->NOW(), $this->mContentId ], $roles, [ $gBitUser->mUserId ] ); - $sql = "SELECT s.`x_group`, x.`xref_id`, x.`last_update_date`, x.`item`, t.`title` AS type_title, - CASE - WHEN x.`end_date` < ? THEN 'history' - ELSE t.`x_group` END AS type_source, - CASE - WHEN x.`xorder` = 0 THEN s.`cross_ref_title` - ELSE s.`cross_ref_title` || '-' || x.`xorder` END - AS source_title, - x.`xref`, x.`xkey`, x.`xkey_ext`, x.`xorder`, x.`data`, - x.`start_date`, x.`end_date`, s.`template`, - pc.`add1` || ',' || pc.`add2` || ',' || pc.`add4` || ',' || pc.`town` AS address - FROM `".BIT_DB_PREFIX."liberty_xref` x - JOIN `".BIT_DB_PREFIX."liberty_xref_item` s ON s.`item` = x.`item` AND s.`content_type_guid` = '".$this->mContentTypeGuid."' - LEFT JOIN `".BIT_DB_PREFIX."address_postcode` pc ON pc.`postcode` = x.`xkey` - JOIN `".BIT_DB_PREFIX."liberty_xref_group` t ON t.`x_group` = s.`x_group` AND t.`content_type_guid` = '".$this->mContentTypeGuid."' - LEFT OUTER JOIN `".BIT_DB_PREFIX."users_roles_map` purm ON ( purm.`user_id`=".$gBitUser->mUserId." ) AND ( purm.`role_id`=s.`role_id` ) - WHERE x.`content_id` = ? AND (s.`role_id` IN(". implode(',', array_fill(0, count($roles), '?')) ." ) OR purm.`user_id`=?) - ORDER BY x.`item`, x.`xorder`"; - $result = $this->mDb->query( $sql, $bindVars ); - if ( $result ) { - while ( $res = $result->fetchRow() ) { - $this->mInfo[$res['type_source']][] = $res; - } - } + public function loadXrefInfo(): void { + if( $this->isValid() && !empty( $this->mContentTypeGuid ) ) { + $this->mXrefInfo = new LibertyXrefInfo( $this->mContentTypeGuid ); + $this->mXrefInfo->load( $this->mContentId ); } } /** - * Loads a single xref record by xref_id and then loads the parent content item. + * Load a single xref row and its parent content item by xref_id. + * + * Used by edit_xref.php to populate the edit form for one row. Loads the + * content item this xref belongs to into $this, then stores the xref's own + * data into mInfo['xref_store'] and mInfo['xref_title'] for template use. + * + * @param int|null $pXrefId liberty_xref.xref_id to load */ public function loadXref( $pXrefId = NULL ): void { if ( BitBase::verifyId( $pXrefId ) ) { @@ -3971,7 +3985,17 @@ class LibertyContent extends LibertyBase implements BitCacheable { } /** - * Stores or updates an xref record for this content item. + * Write one xref row for this content item (insert or update). + * + * Delegates to LibertyXref::store(). On success, reloads the content item + * so mInfo reflects the current DB state, and writes the new xref_id back + * into $pParamHash['xref_id']. + * + * IMPORTANT: always pass a named variable — LibertyXref::store() takes + * &$pParamHash by reference; passing a literal array is a fatal error. + * + * @param array &$pParamHash see LibertyXref::verify() for expected keys + * @return bool true on success */ public function storeXref( &$pParamHash ): bool { $xref = new LibertyXref(); @@ -3990,7 +4014,15 @@ class LibertyContent extends LibertyBase implements BitCacheable { } /** - * Steps (creates a new dated version of) an xref record for this content item. + * Write an xref row using the audit-trail stepping pattern. + * + * Delegates to LibertyXref::stepXref(). The expunge value in $pParamHash + * controls whether the current row is closed and a new one opened (2), + * just closed (1), or updated in place (0). See LibertyXref::stepXref() + * for the full semantics. Reloads the content item on success. + * + * @param array &$pParamHash must include 'xref_id' and 'expunge' + * @return bool true on success */ public function stepXref( &$pParamHash ): bool { $xref = new LibertyXref(); diff --git a/includes/classes/LibertyXref.php b/includes/classes/LibertyXref.php index 2473507..4ba8145 100644 --- a/includes/classes/LibertyXref.php +++ b/includes/classes/LibertyXref.php @@ -9,12 +9,42 @@ namespace Bitweaver\Liberty; use Bitweaver\BitBase; use Bitweaver\BitDate; +/** + * Represents a single row in liberty_xref. + * + * liberty_xref is the live data table of the xref system. Each row attaches a + * typed key-value record to a content item: + * + * content_id — the liberty_content row this xref belongs to + * item — the slot key (matches liberty_xref_item.item), e.g. '#P', 'REQN', 'SGL' + * xorder — sequence within multiple-cardinality items; 0 for single items + * xref — optional FK to another content_id (e.g. linked contact or component) + * xkey — short key value (max 32 chars), e.g. SCREF code, postcode, quantity + * xkey_ext — longer extension of xkey (max 250 chars), e.g. pipe-separated name parts + * data — free-text blob, e.g. notes or a "from" label + * start_date — when this xref became active (NULL = open / not date-bounded) + * end_date — when this xref expired (NULL = still active) + * last_update_date — last write timestamp + * + * This class handles load/verify/store for a single row. For bulk loading of all + * xref rows belonging to a content item, use LibertyXrefGroup / LibertyXrefInfo. + * + * The stepXref() method implements an audit-trail pattern: instead of updating a row + * in place it closes the current row (sets end_date) and opens a new one, preserving + * history. Expired rows are swept into the synthetic 'history' group by LibertyXrefInfo. + */ class LibertyXref extends LibertyBase { + /** x_group value of the loaded xref's item definition */ public $mType; + /** item key of the loaded row (matches liberty_xref_item.item) */ public $mItem; + /** primary key of the loaded liberty_xref row */ public $mXrefId; + /** content_id this xref belongs to */ public $mContentId; + /** BitDate instance used for start/end date conversions */ public $mDate; + /** when set, scopes liberty_xref_item lookups to this content type */ public $mContentTypeGuid = ''; public function __construct( $iXrefId = NULL ) { @@ -29,10 +59,23 @@ class LibertyXref extends LibertyBase { $this->mDate->get_display_offset(); } + /** @return bool true if a row has been loaded (mXrefId is a valid integer) */ public function isValid() { return $this->verifyId( $this->mXrefId ); } + /** + * Load a single liberty_xref row by its primary key. + * + * Joins liberty_xref_item to resolve the group (mType), item display title, + * and template. Also derives source_title by appending xorder to the item + * title for multi-cardinality rows, and computes ignore_start/end_date flags + * so templates can treat NULL dates as "not set". + * + * On success populates mXrefId, mContentId, mType, mItem, and mInfo. + * + * @param int|null $pXrefId liberty_xref.xref_id to load + */ public function load( $pXrefId = NULL ) { if( BitBase::verifyId( $pXrefId ) ) { $guidFilter = !empty( $this->mContentTypeGuid ) ? "AND s.`content_type_guid` = ?" : ''; @@ -62,6 +105,23 @@ class LibertyXref extends LibertyBase { } } + /** + * Validate and normalise the param hash before store(). + * + * Reads raw POST/form values from $pParamHash and builds a clean + * $pParamHash['xref_store'] array ready for associateInsert/Update. + * + * Special flags in the hash: + * fAddXref — new row for a multiple-cardinality item; auto-increments xorder + * fStepXref — audit-trail step: opens a new row as the continuation of this one + * + * Date fields (start_Month/Day/Year/Hour/Minute + ignore_start_date etc.) are + * converted from display timezone to UTC and stored as SQL TIMESTAMP strings. + * Setting ignore_start_date/ignore_end_date to 'on' stores NULL for that date. + * + * @param array &$pParamHash in/out; errors appended to $this->mErrors on failure + * @return bool true if no errors + */ public function verify( &$pParamHash ) { $pParamHash['xref_id'] = ( @$this->verifyId( $pParamHash['xref_id'] ) ) ? (int) $pParamHash['xref_id'] : null; @@ -141,6 +201,20 @@ class LibertyXref extends LibertyBase { return count( $this->mErrors ) == 0; } + /** + * Persist one liberty_xref row (insert or update). + * + * Calls verify() first. Inserts if $pParamHash['xref_id'] is absent; + * updates the matching row otherwise. On insert, allocates a new xref_id + * from liberty_xref_seq and writes it back into $pParamHash['xref_id']. + * Always reloads the row after writing so mInfo reflects the stored state. + * + * IMPORTANT: always pass a named variable — this method takes &$pParamHash + * by reference and will fatal if given a literal array. + * + * @param array &$pParamHash see verify() for expected keys + * @return bool true on success + */ public function store( &$pParamHash = NULL ) { if( $this->verify( $pParamHash ) ) { $table = BIT_DB_PREFIX."liberty_xref"; @@ -160,6 +234,19 @@ class LibertyXref extends LibertyBase { return false; } + /** + * Store an xref row using the audit-trail stepping pattern. + * + * The expunge value in $pParamHash controls the step behaviour: + * 2 — close the current row now (end_date = now) and immediately open a fresh + * continuation row (fStepXref), preserving the full history chain + * 1 — close the current row (end_date = now) with no continuation; the xref + * is retired and will appear in the history group on next load + * 0 — update in place (end_date cleared); standard non-stepping store + * + * @param array &$pParamHash must include 'expunge' and 'xref_id' + * @return bool always true + */ public function stepXref( &$pParamHash = NULL ) { if( isset( $pParamHash["expunge"] ) ) { switch( $pParamHash["expunge"] ) { diff --git a/includes/classes/LibertyXrefGroup.php b/includes/classes/LibertyXrefGroup.php index 1114c88..f8cb4d3 100644 --- a/includes/classes/LibertyXrefGroup.php +++ b/includes/classes/LibertyXrefGroup.php @@ -7,21 +7,46 @@ namespace Bitweaver\Liberty; /** - * One xref group (content_type_guid + x_group) with its raw xref data rows for a content item. - * Defined by liberty_xref_item rows sharing this x_group; data loaded from liberty_xref. - * Active items land in mXrefs; expired items (type_source='history') are returned by load() - * for LibertyXrefInfo to collect into the synthetic history group. + * One xref group for a specific content item. + * + * A group is identified by (content_type_guid, x_group). Its metadata (title, + * sort order, Smarty template, role gate) comes from liberty_xref_group; its live + * data rows come from liberty_xref filtered to that group's items. + * + * loadXrefs() separates rows into two buckets: + * - active rows → stored in mXrefs, rendered by the group's template + * - expired rows → returned to LibertyXrefInfo, which collects them into + * a synthetic 'history' group (sort_order 999) + * + * Instances are created by LibertyXrefInfo::load(); do not instantiate directly + * except in tests or package-level loadXrefInfo() overrides. + * + * Template access: group templates receive the whole object as $xrefGroup and + * iterate $xrefGroup->mXrefs. The first two lines of every group template must be: + * + * {assign var=xrefAllowEdit value=$allow_edit|default:false} + * {assign var=isHistory value=($xrefGroup->mXGroup eq 'history')} */ class LibertyXrefGroup extends LibertyBase { + /** x_group key (e.g. 'address', 'reference', 'quantity', 'history') */ public string $mXGroup; + /** content_type_guid this group belongs to (e.g. 'contact', 'stockmovement') */ public string $mContentTypeGuid; + /** display title from liberty_xref_group.title */ public string $mTitle; + /** render order; liberty_xref_group.sort_order; history group is always 999 */ public int $mSortOrder; + /** Smarty template name from liberty_xref_group.template; null falls back to liberty/list_xref.tpl */ public ?string $mTemplate; + /** role_id gate from liberty_xref_group.role_id; 0 = visible to all */ public int $mRoleId; - /** @var array[] active xref data rows for the current content item */ + /** @var array[] active liberty_xref data rows for the current content item */ public array $mXrefs = []; + /** + * @param array $groupRow row from liberty_xref_group (x_group, title, sort_order, template, role_id) + * @param string $contentTypeGuid content type this group belongs to + */ public function __construct( array $groupRow, string $contentTypeGuid ) { parent::__construct(); $this->mXGroup = $groupRow['x_group']; @@ -33,11 +58,22 @@ class LibertyXrefGroup extends LibertyBase { } /** - * Load xref rows for this group. Active rows go into mXrefs. - * Expired rows (type_source='history') are returned for LibertyXrefInfo to collect. - * Requires mContentTypeGuid — fails loudly if not set. + * Load liberty_xref rows for this group under a given content item. + * + * Queries liberty_xref joined to liberty_xref_item (scoped to this group and + * content type) and address_postcode (for address groups). Role filtering is + * applied against the current user's roles; anonymous gets role_id -1. + * + * Rows whose end_date is in the past are classified as 'history' and returned + * separately so LibertyXrefInfo can accumulate them into the synthetic history + * group. Active rows are stored directly in $this->mXrefs. + * + * Packages that need to enrich rows (e.g. resolving contact titles from xref + * content_ids) should do so by overriding loadXrefInfo() in their own class + * after calling parent::loadXrefInfo(), not by modifying this method. * - * @return array expired rows to be added to the history group + * @param int $contentId liberty_content.content_id to fetch rows for + * @return array[] expired rows (type_source='history') for the caller to collect */ public function loadXrefs( int $contentId ): array { global $gBitUser; diff --git a/includes/classes/LibertyXrefInfo.php b/includes/classes/LibertyXrefInfo.php new file mode 100644 index 0000000..f3f9622 --- /dev/null +++ b/includes/classes/LibertyXrefInfo.php @@ -0,0 +1,87 @@ +<?php +/** + * @package liberty + * @subpackage classes + */ + +namespace Bitweaver\Liberty; + +/** + * Top-level container for all xref groups belonging to one content item. + * + * LibertyXrefInfo::load() is the single entry point for fetching the complete + * xref picture for a content item. It: + * 1. Queries liberty_xref_group for all groups defined for the content type + * (sort_order > 0 only — sort_order 0 is reserved for the 'type' group + * which is loaded separately by loadXrefTypeList()). + * 2. Applies the current user's role filter on each group's role_id gate. + * 3. Creates a LibertyXrefGroup for each row and calls loadXrefs() on it. + * 4. Collects any expired rows returned by loadXrefs() and, if any exist, + * synthesises a 'history' group (sort_order 999) to hold them. + * + * After load(), $mGroups is a keyed array of LibertyXrefGroup objects ordered + * by sort_order. Templates iterate this directly: + * + * {foreach $gXrefInfo->mGroups as $xrefGroup} + * {include file=$gContent->getXrefListTemplate($xrefGroup->mTemplate) xrefGroup=$xrefGroup} + * {/foreach} + * + * Package-level enrichment (resolving contact titles, component details, etc.) + * is done in the package class's loadXrefInfo() override after calling + * parent::loadXrefInfo(), by walking $mXrefInfo->mGroups and mutating rows. + */ +class LibertyXrefInfo { + /** content_type_guid this info object was built for */ + public string $mContentTypeGuid; + /** @var LibertyXrefGroup[] keyed by x_group, ordered by sort_order */ + public array $mGroups = []; + + /** @param string $contentTypeGuid e.g. 'contact', 'stockmovement' */ + public function __construct( string $contentTypeGuid ) { + $this->mContentTypeGuid = $contentTypeGuid; + } + + /** + * Load all visible xref groups and their rows for a content item. + * + * Populates $this->mGroups. Safe to call multiple times — each call + * rebuilds mGroups from scratch. + * + * @param int $contentId liberty_content.content_id + */ + public function load( int $contentId ): void { + global $gBitDb, $gBitUser; + + $roles = array_keys( $gBitUser->mRoles ?? [] ) ?: [-1]; + $userId = $gBitUser->mUserId; + $bindVars = array_merge( $roles, [ $userId ] ); + + $sql = "SELECT g.`x_group`, g.`title`, g.`sort_order`, g.`template`, g.`role_id` + FROM `" . BIT_DB_PREFIX . "liberty_xref_group` g + LEFT OUTER JOIN `" . BIT_DB_PREFIX . "users_roles_map` purm + ON purm.`user_id` = $userId AND purm.`role_id` = g.`role_id` + WHERE g.`content_type_guid` = '{$this->mContentTypeGuid}' + AND g.`sort_order` > 0 + AND (g.`role_id` IN(" . implode( ',', array_fill( 0, count( $roles ), '?' ) ) . ") OR purm.`user_id` = ?) + ORDER BY g.`sort_order`"; + + $result = $gBitDb->query( $sql, $bindVars ); + if( !$result ) return; + + $allHistory = []; + while( $row = $result->fetchRow() ) { + $group = new LibertyXrefGroup( $row, $this->mContentTypeGuid ); + $allHistory = array_merge( $allHistory, $group->loadXrefs( $contentId ) ); + $this->mGroups[$row['x_group']] = $group; + } + + if( !empty( $allHistory ) ) { + $historyGroup = new LibertyXrefGroup( + [ 'x_group' => 'history', 'title' => 'History', 'sort_order' => 999, 'template' => null, 'role_id' => 0 ], + $this->mContentTypeGuid + ); + $historyGroup->mXrefs = $allHistory; + $this->mGroups['history'] = $historyGroup; + } + } +} diff --git a/includes/classes/LibertyXrefType.php b/includes/classes/LibertyXrefType.php index d90fd2a..d54e0c4 100644 --- a/includes/classes/LibertyXrefType.php +++ b/includes/classes/LibertyXrefType.php @@ -8,12 +8,52 @@ namespace Bitweaver\Liberty; use Bitweaver\BitBase; +/** + * Read-only query class for the xref schema tables. + * + * The xref system is defined by two DB tables before any data exists: + * + * liberty_xref_group — one row per logical group of xref slots for a content type + * (e.g. 'address', 'reference', 'quantity'). The group sets + * the display title, sort order, Smarty template, and role gate + * for all items within it. + * + * liberty_xref_item — one row per named slot within a group (e.g. '#P', 'REQN', + * 'SGL'). Defines the item key, display title, cardinality + * (multiple), role gate, and which Smarty template renders it. + * + * Neither table holds user data. Live data lives in liberty_xref. + * + * Methods are split into two groups: + * + * Runtime queries — role-filtered, content-type-scoped. Called via delegate + * methods on LibertyContent (e.g. $gContent->getXrefTypeList()) + * or directly by page files that have already resolved + * the content type guid. + * + * Admin queries — unfiltered, with usage counts. Used by admin pages that + * need the full picture across all roles and content. + */ class LibertyXrefType extends LibertyBase { public function __construct() { parent::__construct(); } + /** + * Return all liberty_xref_item rows, optionally filtered. + * + * Each returned row is augmented with num_entries: the count of live liberty_xref + * rows that use that item key (across all content). Useful for admin listings. + * + * Supported keys in $pOptionHash: + * content_type_guid — restrict to one content type + * active_role — restrict to items visible to one role_id + * item — restrict to one item key + * + * @param array|null $pOptionHash optional filter hash + * @return array[] liberty_xref_item rows with num_entries appended + */ public static function getXrefTypeList( $pOptionHash = NULL ) { global $gBitSystem; @@ -52,7 +92,9 @@ class LibertyXrefType extends LibertyBase { } /** - * Returns the distinct content_type_guid values present in liberty_xref_group. + * Return the distinct content_type_guid values that have at least one group defined. + * + * @return string[] */ public static function getContentTypeGuids(): array { global $gBitSystem; @@ -67,9 +109,221 @@ class LibertyXrefType extends LibertyBase { return $ret; } + // ------------------------------------------------------------------------- + // Runtime queries — role-filtered, content-type-scoped. + // These are the methods called from page files and delegates on LibertyContent. + // ------------------------------------------------------------------------- + + /** + * Return display groups (sort_order > 0) for a content type, filtered to the + * current user's roles. + * + * Used by add-xref pages to build the group selector. Sort_order = 0 is the + * 'type' group (category markers); that is excluded here and loaded separately + * via getContentTypeMarkers(). + * + * @param string $contentTypeGuid e.g. 'contact', 'stockassembly' + * @return array[] liberty_xref_group rows ordered by sort_order + */ + public static function getDisplayGroups( string $contentTypeGuid ): array { + global $gBitSystem, $gBitUser; + $roles = array_keys( $gBitUser->mRoles ?? [] ) ?: [-1]; + $bindVars = array_merge( $roles, [ $gBitUser->mUserId ] ); + $result = $gBitSystem->mDb->query( + "SELECT g.* FROM `".BIT_DB_PREFIX."liberty_xref_group` g + LEFT OUTER JOIN `".BIT_DB_PREFIX."users_roles_map` purm + ON purm.`user_id` = ".$gBitUser->mUserId." AND purm.`role_id` = g.`role_id` + WHERE g.`content_type_guid` = '$contentTypeGuid' AND g.`sort_order` > 0 + AND (g.`role_id` IN(".implode(',', array_fill(0, count($roles), '?')).") OR purm.`user_id` = ?) + ORDER BY g.`sort_order`", + $bindVars + ); + $ret = []; + while( $res = $result->fetchRow() ) { + $ret[] = $res; + } + return $ret; + } + + /** + * Return sort_order=0 item slots for a content type, filtered to the current + * user's roles. + * + * These are top-level type/category markers (e.g. contact's $00/$02+ person/ + * business subtypes). Used by type-selector forms in add_business.php, edit.php + * and similar. + * + * @param string $contentTypeGuid + * @return array[] [{item: string, name: string}, ...] ordered by item key + */ + public static function getTypeMarkers( string $contentTypeGuid ): array { + global $gBitSystem, $gBitUser; + $roles = array_keys( $gBitUser->mRoles ?? [] ) ?: [-1]; + $bindVars = array_merge( $roles, [ $gBitUser->mUserId ] ); + $result = $gBitSystem->mDb->query( + "SELECT g.`cross_ref_title` AS `type_name`, g.`item` + FROM `".BIT_DB_PREFIX."liberty_xref_item` g + JOIN `".BIT_DB_PREFIX."liberty_xref_group` t + ON t.`x_group` = g.`x_group` AND t.`content_type_guid` = '$contentTypeGuid' + LEFT OUTER JOIN `".BIT_DB_PREFIX."users_roles_map` purm + ON purm.`user_id` = ".$gBitUser->mUserId." AND purm.`role_id` = g.`role_id` + WHERE g.`content_type_guid` = '$contentTypeGuid' AND t.`sort_order` = 0 + AND (g.`role_id` IN(".implode(',', array_fill(0, count($roles), '?')).") OR purm.`user_id` = ?) + ORDER BY g.`item`", + $bindVars + ); + $ret = []; + $cnt = 0; + while( $res = $result->fetchRow() ) { + $ret[$cnt]['item'] = $res['item']; + $ret[$cnt++]['name'] = trim( $res['type_name'] ); + } + return $ret; + } + + /** + * Return available item slots for the add-xref type selector. + * + * Three modes controlled by the arguments: + * $xrefTemplate set — all items whose template matches, regardless of group + * $xrefGroup > -1 — items in the group at that sort_order, excluding slots + * already filled for this content item (single-cardinality + * items that already have an active row) + * $xrefGroup == -1 — same but across all groups (sort_order > 0) + * + * Returns ['list' => [item => display_name, ...], 'type' => [item => template, ...]] + * where template defaults to 'generic' when the DB value is empty. + * + * @param string $contentTypeGuid + * @param int $contentId liberty_content.content_id of the current item + * @param int $xrefGroup sort_order of the target group, or -1 for all + * @param string|null $xrefTemplate filter by template name instead of group + * @return array{list: array<string,string>, type: array<string,string>} + */ + public static function getAvailableItems( string $contentTypeGuid, int $contentId, int $xrefGroup = 0, ?string $xrefTemplate = null ): array { + global $gBitSystem; + $db = $gBitSystem->mDb; + if( $xrefTemplate ) { + $result = $db->query( + "SELECT s.`cross_ref_title` AS `type_name`, s.`item`, s.`template` + FROM `".BIT_DB_PREFIX."liberty_xref_item` s + WHERE s.`content_type_guid` = '$contentTypeGuid' AND s.`template` = ? + ORDER BY s.`cross_ref_title`", + [ $xrefTemplate ] + ); + } elseif( $xrefGroup > -1 ) { + $result = $db->query( + "SELECT s.`cross_ref_title` AS `type_name`, s.`item`, s.`template` + FROM `".BIT_DB_PREFIX."liberty_xref_item` s + JOIN `".BIT_DB_PREFIX."liberty_xref_group` t + ON t.`x_group` = s.`x_group` AND t.`content_type_guid` = '$contentTypeGuid' + LEFT JOIN `".BIT_DB_PREFIX."liberty_xref` x + ON x.`item` = s.`item` AND x.`content_id` = ? AND (x.`end_date` IS NULL OR x.`end_date` > CURRENT_TIMESTAMP) + WHERE s.`content_type_guid` = '$contentTypeGuid' AND t.`sort_order` = ? + AND (x.`xref_id` IS NULL OR x.`xorder` > 0) + ORDER BY s.`cross_ref_title`", + [ $contentId, $xrefGroup ] + ); + } else { + $result = $db->query( + "SELECT s.`cross_ref_title` AS `type_name`, s.`item`, s.`template` + FROM `".BIT_DB_PREFIX."liberty_xref_item` s + JOIN `".BIT_DB_PREFIX."liberty_xref_group` t + ON t.`x_group` = s.`x_group` AND t.`content_type_guid` = '$contentTypeGuid' + LEFT JOIN `".BIT_DB_PREFIX."liberty_xref` x + ON x.`item` = s.`item` AND x.`content_id` = ? AND (x.`end_date` IS NULL OR x.`end_date` > CURRENT_TIMESTAMP) + WHERE s.`content_type_guid` = '$contentTypeGuid' AND t.`sort_order` > 0 + AND (x.`xref_id` IS NULL OR x.`xorder` > 0) + ORDER BY s.`cross_ref_title`", + [ $contentId ] + ); + } + $ret = []; + while( $res = $result->fetchRow() ) { + $ret['list'][$res['item']] = trim( $res['type_name'] ); + $ret['type'][$res['item']] = trim( $res['template'] ) !== '' ? trim( $res['template'] ) : 'generic'; + } + return $ret; + } + + /** + * Return the distinct template format names defined across all item slots for + * a content type, filtered to the current user's roles. + * + * Used by the add-xref UI to know which item template types are available. + * Empty template values are normalised to 'generic'. + * + * @param string $contentTypeGuid + * @return string[] + */ + public static function getTemplateFormats( string $contentTypeGuid ): array { + global $gBitSystem, $gBitUser; + $roles = array_keys( $gBitUser->mRoles ?? [] ) ?: [-1]; + $bindVars = array_merge( $roles, [ $gBitUser->mUserId ] ); + $result = $gBitSystem->mDb->query( + "SELECT DISTINCT g.`template` + FROM `".BIT_DB_PREFIX."liberty_xref_item` g + LEFT OUTER JOIN `".BIT_DB_PREFIX."users_roles_map` purm + ON purm.`user_id` = ".$gBitUser->mUserId." AND purm.`role_id` = g.`role_id` + WHERE g.`content_type_guid` = '$contentTypeGuid' + AND (g.`role_id` IN(".implode(',', array_fill(0, count($roles), '?')).") OR purm.`user_id` = ?) + ORDER BY g.`template`", + $bindVars + ); + $ret = []; + while( $res = $result->fetchRow() ) { + $ret[] = trim( $res['template'] ) !== '' ? trim( $res['template'] ) : 'generic'; + } + return $ret; + } + + /** + * Return sort_order=0 type markers for a content item, showing which apply. + * + * Queries all item slots at sort_order=0 for the content type and left-joins + * liberty_xref to show which ones have an active row for the given content item. + * Each row includes 'content_id' (non-null when the marker is set on the item). + * + * @param string $contentTypeGuid + * @param int $contentId liberty_content.content_id of the item to check + * @return array[] + */ + public static function getContentTypeMarkers( string $contentTypeGuid, int $contentId ): array { + global $gBitSystem, $gBitUser; + $roles = array_keys( $gBitUser->mRoles ?? [] ) ?: [-1]; + $bindVars = array_merge( [ $contentId ], $roles, [ $gBitUser->mUserId ] ); + $result = $gBitSystem->mDb->query( + "SELECT r.`item`, r.`cross_ref_title`, d.`content_id` + FROM `".BIT_DB_PREFIX."liberty_xref_item` r + JOIN `".BIT_DB_PREFIX."liberty_xref_group` t + ON t.`x_group` = r.`x_group` AND t.`content_type_guid` = '$contentTypeGuid' + LEFT JOIN `".BIT_DB_PREFIX."liberty_xref` d ON d.`content_id` = ? AND d.`item` = r.`item` + LEFT OUTER JOIN `".BIT_DB_PREFIX."users_roles_map` purm + ON purm.`user_id` = ".$gBitUser->mUserId." AND purm.`role_id` = r.`role_id` + WHERE r.`content_type_guid` = '$contentTypeGuid' AND t.`sort_order` = 0 + AND (r.`role_id` IN(".implode(',', array_fill(0, count($roles), '?')).") OR purm.`user_id` = ?) + ORDER BY r.`item`", + $bindVars + ); + $ret = []; + while( $res = $result->fetchRow() ) { + $ret[] = $res; + } + return $ret; + } + + // ------------------------------------------------------------------------- + // Admin queries — unfiltered, with usage counts. + // ------------------------------------------------------------------------- + /** - * Returns liberty_xref_group rows, optionally filtered by content_type_guid. - * Each row includes num_sources: count of sources defined for that group. + * Return liberty_xref_group rows, optionally filtered by content_type_guid. + * + * Each row is augmented with num_sources: count of liberty_xref_item rows + * defined for that group. Rows are ordered by content_type_guid, sort_order. + * + * @param array|null $pOptionHash optional; supports key 'content_type_guid' + * @return array[] */ public static function getGroupList( $pOptionHash = NULL ): array { global $gBitSystem; |
