Version 4.3.0

This commit is contained in:
Neo committed 2025-12-19 05:47:12 -08:00
1 parent 96997ddd8e
commit fe1acf984a
1349 files changed
+116159 -67711

No files matched your search

+242 -58
View File
@@ -27,7 +27,6 @@ class _Records extends \IPS\Content\Item implements
\IPS\Content\Tags,
\IPS\Content\Followable,
\IPS\Content\Shareable,
\IPS\Content\ReportCenter,
\IPS\Content\ReadMarkers,
\IPS\Content\Views,
\IPS\Content\Ratings,
@@ -36,7 +35,7 @@ class _Records extends \IPS\Content\Item implements
\IPS\Content\Embeddable,
\IPS\Content\MetaData
{
use \IPS\Content\Reactable;
use \IPS\Content\Reactable, \IPS\Content\Reportable;
/**
* @brief Multiton Store
@@ -268,6 +267,12 @@ class _Records extends \IPS\Content\Item implements
public static function loadFromSlug( $slug, $redirectIfSeoTitleIsIncorrect=TRUE, $categoryId=NULL )
{
$slug = trim( $slug, '/' );
/* If the slug is an empty string, then there is nothing to try and load. */
if ( empty( $slug ) )
{
throw new \OutOfRangeException;
}
/* Try the easiest option */
preg_match( '#-r(\d+?)$#', $slug, $matches );
@@ -283,6 +288,12 @@ class _Records extends \IPS\Content\Item implements
{
$url = $record->url();
/* Don't correct the URL if the visitor cannot see the record */
if( !$record->canView() )
{
throw new \OutOfRangeException;
}
/* Redirect to the embed form if necessary */
if( isset( \IPS\Request::i()->do ) and \IPS\Request::i()->do == 'embed' )
{
@@ -394,13 +405,15 @@ class _Records extends \IPS\Content\Item implements
/**
* Definite Article
*
* @param \IPS\Lang|NULL $language The language to use, or NULL for the language of the currently logged in member
* @param array $containerData Basic data about the container. Only includes columns returned by container::basicDataColumns()
* @param \IPS\Lang|NULL $language The language to use, or NULL for the language of the currently logged in member
* @param array $options Options to pass to \IPS\Lang::addToStack
* @return string
*/
public static function _definiteArticle( array $containerData = NULL, \IPS\Lang $lang = NULL )
public static function _definiteArticle( array $containerData = NULL, \IPS\Lang $lang = NULL, $options = array() )
{
$lang = $lang ?: \IPS\Member::loggedIn()->language();
return $lang->addToStack( 'content_db_lang_sl_' . static::$customDatabaseId, FALSE );
return $lang->addToStack( 'content_db_lang_sl_' . static::$customDatabaseId, FALSE, $options );
}
/**
@@ -452,6 +465,12 @@ class _Records extends \IPS\Content\Item implements
{
return true;
}
/* Make sure it does not match the dynamic URL format */
if ( preg_match( '#-r(\d+?)$#', $val ) )
{
throw new \InvalidArgumentException('content_record_slug_not_unique');
}
try
{
@@ -519,7 +538,7 @@ class _Records extends \IPS\Content\Item implements
}
}
$postKey = ( $item ) ? $item->_post_key : md5( uniqid() );
$postKey = ( $item ) ? $item->_post_key : md5( mt_rand() );
if ( $fieldsClass::fixedFieldFormShow( 'record_publish_date' ) AND \IPS\Member::loggedIn()->modPermission( "can_future_publish_content" ) )
{
@@ -1054,7 +1073,7 @@ class _Records extends \IPS\Content\Item implements
if ( isset( $thumbs[ (string) $original ] ) )
{
\IPS\Db::i()->delete( 'cms_database_fields_thumbnails', array( array( 'thumb_original_location=? and thumb_field_id=?', (string) $original, $row->id ) ) );
\IPS\Db::i()->delete( 'cms_database_fields_thumbnails', array( array( 'thumb_original_location=? and thumb_field_id=? and thumb_record_id=?', (string) $original, $row->id, $this->primary_id_field ) ) );
try
{
@@ -1067,16 +1086,17 @@ class _Records extends \IPS\Content\Item implements
'thumb_original_location' => (string) $original,
'thumb_location' => (string) $thumb,
'thumb_field_id' => $row->id,
'thumb_database_id' => static::$customDatabaseId
'thumb_database_id' => static::$customDatabaseId,
'thumb_record_id' => $this->primary_id_field
) );
}
catch ( \Exception $e ) { }
}
catch ( \Exception $e ) { }
}
/* Remove any thumbnails if the original has been removed */
$orphans = iterator_to_array( \IPS\Db::i()->select( '*', 'cms_database_fields_thumbnails', array( array( 'thumb_field_id=?', $row->id ), array( \IPS\Db::i()->in( 'thumb_original_location', explode( ',', $this->$field ), TRUE ) ) ) ) );
$orphans = iterator_to_array( \IPS\Db::i()->select( '*', 'cms_database_fields_thumbnails', array( array( 'thumb_record_id=?', $this->primary_id_field ), array( 'thumb_field_id=?', $row->id ), array( \IPS\Db::i()->in( 'thumb_original_location', explode( ',', $this->$field ), TRUE ) ) ) ) );
if ( count( $orphans ) )
{
@@ -1089,7 +1109,7 @@ class _Records extends \IPS\Content\Item implements
catch ( \Exception $e ) { }
}
\IPS\Db::i()->delete( 'cms_database_fields_thumbnails', array( array( 'thumb_field_id=?', $row->id ), array( \IPS\Db::i()->in( 'thumb_original_location', explode( ',', $this->$field ), TRUE ) ) ) );
\IPS\Db::i()->delete( 'cms_database_fields_thumbnails', array( array( 'thumb_record_id=?', $this->primary_id_field ), array( 'thumb_field_id=?', $row->id ), array( \IPS\Db::i()->in( 'thumb_original_location', explode( ',', $this->$field ), TRUE ) ) ) );
}
}
}
@@ -1408,7 +1428,7 @@ class _Records extends \IPS\Content\Item implements
{
foreach( $fieldValues[ $fieldName ] as $fileName )
{
$obj = \IPS\File::get( 'cms_Records', $filename );
$obj = \IPS\File::get( 'cms_Records', $fileName );
if ( $obj->isImage() )
{
$attachments[] = array( 'cms_Records' => $fileName );
@@ -1436,7 +1456,7 @@ class _Records extends \IPS\Content\Item implements
*/
public function get__post_key()
{
return ! empty( $this->post_key ) ? $this->post_key : md5( uniqid() );
return ! empty( $this->post_key ) ? $this->post_key : md5( mt_rand() );
}
/**
@@ -1778,11 +1798,14 @@ class _Records extends \IPS\Content\Item implements
{
unset( static::$multitons[ $this->primary_id_field ] );
foreach( static::$multitonMap as $field => $data )
foreach( static::$multitonMap as $fieldKey => $data )
{
if ( isset( static::$multitonMap[ $field ][ $this->primary_id_field ] ) )
foreach( $data as $fieldValue => $primaryId )
{
unset( static::$multitonMap[ $field ][ $this->primary_id_field ] );
if( $primaryId == $this->primary_id_field )
{
unset( static::$multitonMap[ $fieldKey ][ $fieldValue ] );
}
}
}
@@ -1976,7 +1999,7 @@ class _Records extends \IPS\Content\Item implements
* @param bool $includeDeleted Include deleted content.
* @return array|NULL|\IPS\Content\Comment If $limit is 1, will return \IPS\Content\Comment or NULL for no results. For any other number, will return an array.
*/
public function comments( $limit=NULL, $offset=NULL, $order='date', $orderDirection='asc', $member=NULL, $includeHiddenComments=NULL, $cutoff=NULL, $extraWhereClause=NULL, $bypassCache=FALSE, $includeDeleted=FALSE )
public function comments( $limit=NULL, $offset=NULL, $order='date', $orderDirection='asc', $member=NULL, $includeHiddenComments=NULL, $cutoff=NULL, $extraWhereClause=NULL, $bypassCache=FALSE, $includeDeleted=FALSE, $canViewWarn=NULL )
{
if ( $this->useForumComments() )
{
@@ -2053,7 +2076,7 @@ class _Records extends \IPS\Content\Item implements
* @param bool $includeDeleted Include deleted content
* @return array|NULL|\IPS\Content\Comment If $limit is 1, will return \IPS\Content\Comment or NULL for no results. For any other number, will return an array.
*/
public function reviews( $limit=NULL, $offset=NULL, $order=NULL, $orderDirection='desc', $member=NULL, $includeHiddenReviews=NULL, $cutoff=NULL, $extraWhereClause=NULL, $includeDeleted=FALSE )
public function reviews( $limit=NULL, $offset=NULL, $order=NULL, $orderDirection='desc', $member=NULL, $includeHiddenReviews=NULL, $cutoff=NULL, $extraWhereClause=NULL, $includeDeleted=FALSE, $canViewWarn=NULL )
{
$where = array( array( 'review_database_id=?', static::$customDatabaseId ) );
@@ -2074,7 +2097,19 @@ class _Records extends \IPS\Content\Item implements
}
if ( static::database()->options['comments'] )
{
$tabs['comments'] = \IPS\Member::loggedIn()->language()->addToStack( 'cms_comment_count', TRUE, array( 'pluralize' => array( $this->mapped('num_comments') ) ) );
$count = $this->mapped('num_comments');
if ( \IPS\Application::appIsEnabled('forums') and $this->_forum_comments and $topic = $this->topic() )
{
if ( $count != ( $topic->posts - 1 ) )
{
$this->record_comments = $topic->posts - 1;
$this->save();
}
$count = ( $topic->posts - 1 ) > 0 ? $topic->posts - 1 : 0;
}
$tabs['comments'] = \IPS\Member::loggedIn()->language()->addToStack( 'cms_comment_count', TRUE, array( 'pluralize' => array( $count ) ) );
}
return $tabs;
@@ -2109,7 +2144,12 @@ class _Records extends \IPS\Content\Item implements
*/
public static function moderateNewItems( \IPS\Member $member, \IPS\Node\Model $container = NULL )
{
return ( static::database()->record_approve and !$member->group['g_avoid_q'] ) or parent::moderateNewItems( $member, $container );
if ( static::database()->record_approve and !$member->group['g_avoid_q'] )
{
return !static::modPermission( 'approve', $member, $container );
}
return parent::moderateNewItems( $member, $container );
}
/**
@@ -2132,9 +2172,13 @@ class _Records extends \IPS\Content\Item implements
public function moderateNewReviews( \IPS\Member $member )
{
return ( static::database()->options['reviews_mod'] and !$member->group['g_avoid_q'] ) or parent::moderateNewReviews( $member );
}
/**
* @brief Skip topic creation, useful if the topic may already exist
*/
public static $skipTopicCreation = FALSE;
/**
* Create from form
*
@@ -2147,7 +2191,7 @@ class _Records extends \IPS\Content\Item implements
{
$record = parent::createFromForm( $values, $container, $sendNotification );
if ( \IPS\Application::appIsEnabled('forums') and $record->_forum_record and $record->_forum_forum and ! $record->hidden() and ! $record->record_future_date )
if ( !static::$skipTopicCreation and \IPS\Application::appIsEnabled('forums') and $record->_forum_record and $record->_forum_forum and ! $record->hidden() and ! $record->record_future_date )
{
try
{
@@ -2346,7 +2390,59 @@ class _Records extends \IPS\Content\Item implements
$topic->changeAuthor( $newAuthor );
}
}
/**
* Get last comment author
* Overloaded for the bump on edit shenanigans
*
* @return \IPS\Member
* @throws \BadMethodCallException
*/
public function lastCommenter()
{
if ( ( static::database()->_comment_bump & ( \IPS\cms\Databases::BUMP_ON_EDIT + \IPS\cms\Databases::BUMP_ON_COMMENT ) and $this->record_edit_time > 0 and $this->record_edit_time > $this->record_last_comment ) OR
( ( static::database()->_comment_bump & \IPS\cms\Databases::BUMP_ON_EDIT ) and !( static::database()->_comment_bump & ( \IPS\cms\Databases::BUMP_ON_EDIT + \IPS\cms\Databases::BUMP_ON_COMMENT ) ) and $this->record_edit_time > 0 ) )
{
try
{
$this->_lastCommenter = \IPS\Member::load( $this->record_edit_member_id );
return $this->_lastCommenter;
}
catch( \Exception $e ) { }
}
return parent::lastCommenter();
}
/**
* Is this topic linked to a record?
*
* @param \IPS\forums\Topic $topic Forums topic
* @return boolean
*/
public static function topicIsLinked( $topic )
{
foreach( \IPS\cms\Databases::databases() as $database )
{
try
{
if ( $database->forum_record and $database->forum_forum == $topic->container()->_id )
{
$class = '\IPS\cms\Records' . $database->id;
$record = $class::load( $topic->tid, 'record_topicid' );
if ( $record->_forum_record )
{
return TRUE;
}
}
}
catch( \Exception $e ) { }
}
return FALSE;
}
/**
* Get Topic (checks member's permissions)
*
@@ -2355,7 +2451,7 @@ class _Records extends \IPS\Content\Item implements
*/
public function topic( $checkPerms=TRUE )
{
if ( \IPS\Application::appIsEnabled('forums') and $this->record_topicid )
if ( \IPS\Application::appIsEnabled('forums') and $this->_forum_record and $this->record_topicid )
{
try
{
@@ -2526,33 +2622,56 @@ class _Records extends \IPS\Content\Item implements
return $fields;
}
/**
* @brief Store the comment page count otherwise $topic->posts is reduced by 1 each time it is called
*/
protected $recordCommentPageCount = NULL;
/**
* Get comment page count
*
* @param bool $recache TRUE to recache the value
* @return int
*/
public function commentPageCount( $recache=FALSE )
{
if ( $this->useForumComments() )
if ( $this->recordCommentPageCount === NULL or $recache === TRUE )
{
try
if ( $this->useForumComments() )
{
$topic = $this->topic();
if( $topic !== NULL )
try
{
$topic->posts = $topic->posts - 1;
return $topic->commentPageCount();
$topic = $this->topic();
if( $topic !== NULL )
{
/* Store the real count so it is not accidentally written as the actual value */
$realCount = $topic->posts;
/* Compensate for the first post (which is actually the record) */
$topic->posts = ( $topic->posts - 1 ) > 0 ? $topic->posts - 1 : 0;
/* Get our page count considering all of that */
$this->recordCommentPageCount = $topic->commentPageCount();
/* Reset the count back to the real count */
$topic->posts = $realCount;
}
else
{
$this->recordCommentPageCount = 0;
}
}
return 0;
catch( \Exception $e ) { }
}
else
{
$this->recordCommentPageCount = parent::commentPageCount( $recache );
}
catch( \Exception $e ) { }
}
return parent::commentPageCount();
return $this->recordCommentPageCount;
}
/**
@@ -2590,7 +2709,28 @@ class _Records extends \IPS\Content\Item implements
/* We have an attached topic, but we don't want to delete the topic so remove commentClass otherwise we'll delete posts */
static::$commentClass = NULL;
}
/* Remove Record Image And Record Thumb Image */
if ( $this->record_image )
{
try
{
\IPS\File::get( 'cms_Records', $this->record_image )->delete();
}
catch( \Exception $e ){}
}
if ( $this->record_image_thumb )
{
try
{
\IPS\File::get( 'cms_Records', $this->record_image_thumb )->delete();
}
catch ( \Exception $e ) { }
}
/* Remove any reciprocal linking */
\IPS\Db::i()->delete( 'cms_database_fields_reciprocal_map', array( 'map_foreign_item_id=? or map_origin_item_id=?', $this->_id, $this->_id ) );
@@ -2610,7 +2750,6 @@ class _Records extends \IPS\Content\Item implements
*/
public function canView( $member=NULL )
{
if ( !parent::canView( $member ) )
{
return FALSE;
@@ -2626,9 +2765,10 @@ class _Records extends \IPS\Content\Item implements
return FALSE;
}
$member = $member ?: \IPS\Member::loggedIn();
if ( !$this->container()->can_view_others and !$member->modPermission( 'can_content_view_others_records' ) )
{
$member = $member ?: \IPS\Member::loggedIn();
if ( $member != $this->author() )
{
return FALSE;
@@ -2648,19 +2788,44 @@ class _Records extends \IPS\Content\Item implements
*/
public function canEdit( $member=NULL )
{
if ( ( ( static::database()->options['indefinite_own_edit'] AND \IPS\Member::loggedIn()->member_id === $this->member_id ) OR ( \IPS\Member::loggedIn()->member_id and static::database()->all_editable ) ) AND ! $this->locked() AND ! $this->hidden() )
$member = $member ?: \IPS\Member::loggedIn();
if ( ( ( static::database()->options['indefinite_own_edit'] AND $member->member_id === $this->member_id ) OR ( $member->member_id and static::database()->all_editable ) ) AND ! $this->locked() AND in_array( $this->hidden(), array( 0, 1 ) ) )
{
return TRUE;
}
if ( parent::canEdit( $member ) )
{
/* Test against specific perms for this category */
return $this->container()->can( 'edit', $member );
return $this->container()->can( 'edit', $member ) or $this->container()->modPermission( 'edit', $member );
}
return FALSE;
}
/**
* Can edit title?
*
* @param \IPS\Member|NULL $member The member to check for (NULL for currently logged in member)
* @return bool
*/
public function canEditTitle( $member=NULL )
{
if ( $this->canEdit( $member ) )
{
try
{
$class = '\IPS\cms\Fields' . static::database()->id;
$field = $class::load( static::database()->field_title );
return $field->can( 'edit', $member );
}
catch( \Exception $e )
{
return FALSE;
}
}
return FALSE;
}
/**
* Can move?
@@ -3013,24 +3178,19 @@ class _Records extends \IPS\Content\Item implements
return $lastComment;
}
/**
* Search Index Permissions
* If we don't have a page, we don't want to add this to the search index
* Deletion log Permissions
* Usually, this is the same as searchIndexPermissions. However, some applications may restrict searching but
* still want to allow delayed deletion log viewing and searching
*
* @return string Comma-delimited values or '*'
* @li Number indicates a group
* @li Number prepended by "m" indicates a member
* @li Number prepended by "s" indicates a social group
*/
public function searchIndexPermissions()
public function deleteLogPermissions()
{
/* We don't want to index items in databases with search disabled */
if( ! static::database()->search )
{
return NULL;
}
if( ! $this->container()->can_view_others )
{
$return = $this->container()->searchIndexPermissions();
@@ -3059,10 +3219,31 @@ class _Records extends \IPS\Content\Item implements
return NULL;
}
}
/**
* Search Index Permissions
* If we don't have a page, we don't want to add this to the search index
*
* @return string Comma-delimited values or '*'
* @li Number indicates a group
* @li Number prepended by "m" indicates a member
* @li Number prepended by "s" indicates a social group
*/
public function searchIndexPermissions()
{
/* We don't want to index items in databases with search disabled */
if( ! static::database()->search )
{
return NULL;
}
return $this->deleteLogPermissions();
}
/**
* Get output for API
*
* @param \IPS\Member|NULL $authorizedMember The member making the API request or NULL for API Key / client_credentials
* @return array
* @apiresponse int id ID number
* @apiresponse string title Title
@@ -3082,15 +3263,17 @@ class _Records extends \IPS\Content\Item implements
* @apiresponse bool featured Event is featured
* @apiresponse string url URL
* @apiresponse float rating Average Rating
* @apiresponse string image Record Image
* @apiresponse \IPS\forums\Topic topic The topic
*/
public function apiOutput()
public function apiOutput( \IPS\Member $authorizedMember = NULL )
{
return array(
'id' => $this->primary_id_field,
'title' => $this->_title,
'category' => $this->container() ? $this->container()->apiOutput() : null,
'fields' => $this->fieldValues(),
'author' => $this->author()->apiOutput(),
'author' => $this->author()->apiOutput( $authorizedMember ),
'date' => \IPS\DateTime::ts( $this->record_saved )->rfc3339(),
'description' => $this->content(),
'comments' => $this->record_comments,
@@ -3104,7 +3287,8 @@ class _Records extends \IPS\Content\Item implements
'featured' => (bool) $this->mapped('featured'),
'url' => (string) $this->url(),
'rating' => $this->averageRating(),
'image' => $this->record_image ? (string) \IPS\File::get( 'cms_Records', $this->record_image )->url : null
'image' => $this->record_image ? (string) \IPS\File::get( 'cms_Records', $this->record_image )->url : null,
'topic' => $this->topicid ? $this->topic()->apiOutput( $authorizedMember ) : NULL,
);
}
@@ -3205,7 +3389,7 @@ class _Records extends \IPS\Content\Item implements
public function embedContent( $params )
{
\IPS\Output::i()->cssFiles = array_merge( \IPS\Output::i()->cssFiles, \IPS\Theme::i()->css( 'embed.css', 'cms', 'front' ) );
return \IPS\Theme::i()->getTemplate( 'global', 'cms' )->embedRecord( $this, $this->container(), $this->url()->setQueryString( $params ) );
return \IPS\Theme::i()->getTemplate( 'global', 'cms' )->embedRecord( $this, $this->url()->setQueryString( $params ) );
}
/**