summaryrefslogtreecommitdiff
path: root/includes
diff options
context:
space:
mode:
authorLester Caine <lester@lsces.co.uk>2026-06-06 15:03:30 +0100
committerLester Caine <lester@lsces.co.uk>2026-06-06 15:03:30 +0100
commitbe5df3df206bd49417ef0d5a68033e22de747e8b (patch)
treee68d42acb150ca2267f187491ecc522a1b9dcb08 /includes
parent87ffc50fc40575ef2ade7aac80b274c2e2c542e0 (diff)
downloadliberty-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-xincludes/classes/LibertyContent.php312
-rw-r--r--includes/classes/LibertyXref.php87
-rw-r--r--includes/classes/LibertyXrefGroup.php54
-rw-r--r--includes/classes/LibertyXrefInfo.php87
-rw-r--r--includes/classes/LibertyXrefType.php260
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;