Seditio Source
Root |
./othercms/ips_4.3.4/applications/cms/sources/Categories/Categories.php
<?php
/**
 * @brief        Categories Model
 * @author        <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
 * @copyright    (c) Invision Power Services, Inc.
 * @license        https://www.invisioncommunity.com/legal/standards/
 * @package        Invision Community
 * @subpackage    Content
 * @since        8 April 2014
 */

namespace IPS\cms;

/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
   
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
    exit;
}

/**
 * @brief Categories Model
 */
class _Categories extends \IPS\Node\Model implements \IPS\Node\Permissions
{
   
/**
     * @brief    Multiton Store
     */
   
protected static $multitons = array();
   
   
/**
     * @brief    [Records] Custom Database Id
     */
   
public static $customDatabaseId = NULL;
   
   
/**
     * @brief    [Records] Content item class
     */
   
public static $contentItemClass = NULL;
   
   
/**
     * @brief    [ActiveRecord] Database Table
     */
   
public static $databaseTable = 'cms_database_categories';
   
   
/**
     * @brief    [ActiveRecord] Database Prefix
     */
   
public static $databasePrefix = 'category_';
   
   
/**
     * @brief    [ActiveRecord] ID Database Column
     */
   
public static $databaseColumnId = 'id';

   
/**
     * @brief    [ActiveRecord] Database ID Fields
     */
   
protected static $databaseIdFields = array('category_furl_name', 'category_full_path');
   
   
/**
     * @brief    [ActiveRecord] Multiton Map
     */
   
protected static $multitonMap    = array();
   
   
/**
     * @brief    [Node] Parent ID Database Column
     */
   
public static $databaseColumnParent = 'parent_id';
   
   
/**
     * @brief    [Node] Parent ID Database Column
     */
   
public static $databaseColumnOrder = 'position';
   
   
/**
     * @brief    [Node] Show forms modally?
     */
   
public static $modalForms = FALSE;
   
   
/**
     * @brief    [Node] Sortable?
     */
   
public static $nodeSortable = TRUE;
   
   
/**
     * @brief    [Node] Node Title
     */
   
public static $nodeTitle = 'r__categories';
   
   
/**
     * @brief    [Node] Title prefix.  If specified, will look for a language key with "{$key}_title" as the key
     */
   
public static $titleLangPrefix = 'content_cat_name_';

   
/**
     * @brief    [Node] Description suffix.  If specified, will look for a language key with "{$titleLangPrefix}_{$id}_{$descriptionLangSuffix}" as the key
     */
   
public static $descriptionLangSuffix = '_desc';
   
   
/**
     * @brief    [Node] ACP Restrictions
     * @code
     array(
     'app'        => 'core',                // The application key which holds the restrictrions
     'module'    => 'foo',                // The module key which holds the restrictions
     'map'        => array(                // [Optional] The key for each restriction - can alternatively use "prefix"
     'add'            => 'foo_add',
     'edit'            => 'foo_edit',
     'permissions'    => 'foo_perms',
     'delete'        => 'foo_delete'
     ),
     'all'        => 'foo_manage',        // [Optional] The key to use for any restriction not provided in the map (only needed if not providing all 4)
     'prefix'    => 'foo_',                // [Optional] Rather than specifying each  key in the map, you can specify a prefix, and it will automatically look for restrictions with the key "[prefix]_add/edit/permissions/delete"
     * @endcode
     */
   
protected static $restrictions = array(
           
'app'        => 'cms',
           
'module'    => 'databases',
           
'prefix'     => 'categories_'
   
);
   
   
/**
     * @brief    [Node] App for permission index
     */
   
public static $permApp = 'cms';
   
   
/**
     * @brief    [Node] Type for permission index
     */
   
public static $permType = 'categories';
   
   
/**
     * @brief    The map of permission columns
     */
   
public static $permissionMap = array(
           
'view'                 => 'view',
           
'read'                => 2,
           
'add'                => 3,
           
'edit'                => 4,
           
'reply'                => 5,
           
'review'            => 7,
           
'rate'                => 6
   
);
   
   
/**
     * @brief    [Node] Moderator Permission
     */
   
public static $modPerm = 'cms';
   
   
/**
     * @brief    [Node] Prefix string that is automatically prepended to permission matrix language strings
     */
   
public static $permissionLangPrefix = 'perm_content_category_';
   
   
/**
     * @brief    [Page] Loaded pages from paths
     */
   
protected static $loadedCatsFromPath = array();

   
/**
     * @brief     [Records] Database objects
     */
   
protected static $database = array();

   
/**
     * @brief   Latest posted record
     */
   
protected static $latestRecordAdded = NULL;

   
/**
     * Returns a database category object (or NULL) based on the path
     *
     * @param    string    $path        Path /like/this/maybearecordhere-r1234
     * @param   int        $databaseId    Database ID to look up
     * @return    NULL|\IPS\cms\Categories object
     */
   
public static function loadFromPath( $path, $databaseId=NULL )
    {
       
$path = trim( $path, '/' );
   
        if ( !
array_key_exists( $path, static::$loadedCatsFromPath ) )
        {
            static::
$loadedCatsFromPath[ $path ] = NULL;
               
           
/* Try the simplest option */
           
try
            {
               
$where = ( $databaseId === NULL ) ? NULL : array( 'category_database_id=?', $databaseId );
                static::
$loadedCatsFromPath[ $path ] =  static::load( $path, 'category_full_path', $where );
            }
            catch ( \
OutOfRangeException $e )
            {
               
/* May contain a record name */
               
$where = ( $databaseId === NULL ) ? array( '? LIKE CONCAT( TRIM(TRAILING \'/\' FROM category_full_path), \'/\', \'%\')', rtrim( $path, '/' ) . '/' ) : array( 'category_database_id=? AND ? LIKE CONCAT( TRIM(TRAILING \'/\' FROM category_full_path), \'/\', \'%\')', $databaseId, rtrim( $path, '/' ) . '/' );

                foreach(
                    \
IPS\Db::i()->select(
                        static::
$databaseTable . '.*, core_permission_index.perm_id, core_permission_index.perm_view, core_permission_index.perm_2, core_permission_index.perm_3, core_permission_index.perm_4, core_permission_index.perm_5, core_permission_index.perm_6, core_permission_index.perm_7',
                        static::
$databaseTable,
                       
$where,
                       
'LENGTH(category_full_path) DESC'
                   
)->join(
                           
'core_permission_index',
                            array(
"core_permission_index.app=? AND core_permission_index.perm_type=? AND core_permission_index.perm_type_id=" . static::$databaseTable . "." . static::$databasePrefix . static::$databaseColumnId, static::$permApp, static::$permType )
                    )
                    as
$meow )
                {
                    static::
$loadedCatsFromPath[ $path ] = static::constructFromData( $meow );

                    break;
                }
            }
        }
   
        return static::
$loadedCatsFromPath[ $path ];
    }

   
/**
     * Returns the database parent
     *
     * @return \IPS\cms\Databases
     */
   
public static function database()
    {
        if ( !isset( static::
$database[ static::$customDatabaseId ] ) )
        {
            static::
$database[ static::$customDatabaseId ] = \IPS\cms\Databases::load( static::$customDatabaseId );
        }

        return static::
$database[ static::$customDatabaseId ];
    }
   
   
/**
     * Test to see if this is a valid container ID
     *
     * @param    int        $id        Container ID
     * @reply boolean
     */
   
public static function isValidContainerId( $id )
    {
        if ( static::
$containerIds === NULL )
        {
            static::
$containerIds = iterator_to_array( \IPS\Db::i()->select( 'category_id', static::$databaseTable, array( array( 'category_database_id=?', static::$customDatabaseId ) ) ) );
        }

        return
in_array( $id, static::$containerIds );
    }

   
/**
     * @brief Cache of categories we've fetched
     */
   
protected static $cache = array();
   
   
/**
     * Fetch All Root Nodes
     *
     * @param    string|NULL            $permissionCheck    The permission key to check for or NULl to not check permissions
     * @param    \IPS\Member|NULL    $member                The member to check permissions for or NULL for the currently logged in member
     * @param    mixed                $where                Additional WHERE clause
     * @return    array
     */
   
public static function roots( $permissionCheck='view', $member=NULL, $where=array() )
    {
        if ( static::
$customDatabaseId !== NULL )
        {
           
$where[] = array('category_database_id=?', static::$customDatabaseId );
        }
       
        return
parent::roots( $permissionCheck, $member, $where );
    }
   
   
/**
     * [Node] Fetch Child Nodes
     *
     * @param    string|NULL            $permissionCheck    The permission key to check for or NULL to not check permissions
     * @param    \IPS\Member|NULL    $member                The member to check permissions for or NULL for the currently logged in member
     * @param    bool                $subnodes            Include subnodes? NULL to *only* check subnodes
     * @param    array|NULL            $skip                Children IDs to skip
     * @param    mixed                $_where                Additional WHERE clause
     * @return    array
     */
   
public function children( $permissionCheck='view', $member=NULL, $subnodes=TRUE, $skip=null, $_where=array() )
    {
       
$permissionCheck = ( \IPS\Dispatcher::hasInstance() AND \IPS\Dispatcher::i()->controllerLocation === 'admin' ) ? NULL : $permissionCheck;
       
        return
parent::children( $permissionCheck, $member, $subnodes, $skip, $_where );
    }
   
   
/**
     * Resets a category path
     *
     * @param     int     $categoryId        Category ID to reset
     * @return    void
     */
   
public static function resetPath( $categoryId )
    {
        try
        {
           
$category = static::load( $categoryId );
        }
        catch ( \
OutOfRangeException $ex )
        {
            throw new \
OutOfRangeException;
        }
   
       
$category->setFullPath();
    }

   
/**
     * Ensure there aren't any collision issues.
     *
     * @param   string  $path   Path to check
     * @return  boolean
     */
   
static public function isFurlCollision( $path )
    {
       
$path  = trim( $path , '/');
       
$bits  = explode( '/', $path );
       
$root  = $bits[0];
       
       
/* _ is here due to IP.Board 3.x using that to denote the articles database (eg. articles.html/_/category/record-r1/), which we need to redirect from for records and articles. */
       
if ( in_array( $root, array( 'submit', '_' ) ) )
        {
            return
TRUE;
        }

        return
FALSE;
    }
   
   
/**
     * Set the "extra" field
     *
     * @param string|array $value
     * @return void
     */
   
public function set_fields( $value )
    {
       
$this->_data['fields'] = ( is_array( $value ) ? json_encode( $value ) : $value );
    }
   
   
/**
     * Get the "extra" field
     *
     * @return array
     */
   
public function get_fields()
    {
        return (
$this->_data['fields'] === '*' OR $this->_data['fields'] === NULL ) ? '*' : json_decode( $this->_data['fields'], TRUE );
    }
   
   
/**
     * [ActiveRecord] Duplicate
     *
     * @return    void
     */
   
public function __clone()
    {
       
parent::__clone();
       
        if(
$this->skipCloneDuplication === TRUE )
        {
            return;
        }
       
       
$this->furl_name .= '_' . $this->id;
       
$this->save();

       
$this->setFullPath();
    }

   
/**
     * Retrieve an array of IDs a member has posted in.
     *
     * @param    \IPS\Member|NULL    $member    The member (NULL for currently logged in member)
     * @param    array|NULL            $inSet    If supplied, checks will be restricted to only the ids provided
     * @param   array|NULL          $additionalWhere    Additional where clause
     * @param    array|NULL            $commentJoinWhere    Additional join clause for comments table
     * @return    array                An array of content item ids
     */
   
public function contentPostedIn( $member=NULL, $inSet=NULL, $additionalWhere=NULL, $commentJoinWhere=NULL )
    {
       
$database = \IPS\cms\Databases::load( $this->database_id );
       
        if (
$database->forum_record and $database->forum_forum )
        {
            return array();
        }
       
       
/* What about local category forum sync? */
       
if ( $this->forum_record and $this->forum_forum )
        {
            return array();
        }
       
       
$contentItemClass = static::$contentItemClass;
       
$commentClass     = $contentItemClass::$commentClass;

        return
parent::contentPostedIn( $member, $inSet, NULL, $commentClass::commentWhere() );
    }

   
/**
     * [Node] Add/Edit Form
     *
     * @param    \IPS\Helpers\Form    $form    The form
     * @return    void
     */
   
public function form( &$form )
    {
       
/* Build form */
       
$form->addTab( 'content_content_form_tab__config' );
       
       
$form->add( new \IPS\Helpers\Form\Translatable( 'category_name', NULL, TRUE, array(
               
'app'          => 'cms',
               
'key'          => ( $this->id ? "content_cat_name_" .  $this->id : NULL )
        ) ) );
       
        if ( !
$this->id )
        {
           
$form->add( new \IPS\Helpers\Form\YesNo( 'category_furl_name_choice', FALSE, FALSE, array(
                   
'togglesOn' => array('category_furl_name')
            ),
NULL, NULL, NULL, 'category_furl_name_choice' ) );
        }
       
       
$form->add( new \IPS\Helpers\Form\Text( 'category_furl_name', $this->furl_name, FALSE, array(), function( $val )
        {
           
/* Make sure key is unique */
           
if ( empty( $val ) )
            {
                return
true;
            }
           
            if ( \
IPS\Request::i()->category_parent_id == 0 and \IPS\cms\Categories::isFurlCollision( $val ) )
            {
                throw new \
InvalidArgumentException('content_cat_furl_collision');
            }
           
            try
            {
               
$cat = \IPS\Db::i()->select( '*', 'cms_database_categories', array( 'category_database_id=? and category_parent_id=? and category_furl_name=?', \IPS\Request::i()->database_id, \IPS\Request::i()->category_parent_id, $val ) )->first();
            }
            catch( \
UnderflowException $ex )
            {
               
/* Nuffink matches */
               
return true;
            }
           
            if ( isset( \
IPS\Request::i()->id ) )
            {
                if (
$cat['category_id'] != \IPS\Request::i()->id )
                {
                    throw new \
InvalidArgumentException('content_cat_furl_not_unique');
                }
            }
            else
            {
                throw new \
InvalidArgumentException('content_cat_furl_not_unique');
            }
           
            return
true;
        },
NULL, NULL, 'category_furl_name' ) );
       
       
$form->add( new \IPS\Helpers\Form\Translatable( 'category_description', NULL, FALSE, array(
               
'app'          => 'cms',
               
'key'          => ( $this->id ? "content_cat_name_" .  $this->id . "_desc" : NULL )
        ) ) );

       
$class = get_called_class();

       
$form->add( new \IPS\Helpers\Form\Node( 'category_parent_id', ( ! $this->id ) ? ( isset( \IPS\Request::i()->parent ) ? \IPS\Request::i()->parent : 0 ) : $this->parent_id, FALSE, array(
           
'class'              => '\IPS\cms\Categories' . \IPS\Request::i()->database_id,
           
'disabled'          => false,
           
'zeroVal'         => 'node_no_parent',
           
'permissionCheck' => function( $node ) use ( $class )
            {
                if( isset(
$class::$subnodeClass ) AND $class::$subnodeClass AND $node instanceof $class::$subnodeClass )
                {
                    return
FALSE;
                }
                   
                return !isset( \
IPS\Request::i()->id ) or ( $node->id != \IPS\Request::i()->id and !$node->isChildOf( $node::load( \IPS\Request::i()->id ) ) );
            }
        ) ) );
       
       
$form->add( new \IPS\Helpers\Form\YesNo( 'category_show_records', $this->id ? $this->show_records : TRUE, FALSE, array(), NULL, NULL, NULL, 'category_show_records' ) );
       
$form->add( new \IPS\Helpers\Form\YesNo( 'category_allow_rating', $this->allow_rating, FALSE, array(), NULL, NULL, NULL, 'category_allow_rating' ) );
       
$form->add( new \IPS\Helpers\Form\YesNo( 'category_has_perms', $this->has_perms, FALSE, array(), NULL, NULL, NULL, 'category_has_perms' ) );
       
$form->add( new \IPS\Helpers\Form\YesNo( 'category_can_view_others', $this->id ? $this->can_view_others : TRUE, FALSE, array(), NULL, NULL, NULL, 'category_can_view_others' ) );

       
$form->addHeader('cms_categories_header_display');
       
$templatesList     = array( 0 => \IPS\Member::loggedIn()->language()->addToStack('cms_categories_use_database') );
       
$templatesDisplay  = array( 0 => \IPS\Member::loggedIn()->language()->addToStack('cms_categories_use_database') );

        foreach( \
IPS\cms\Templates::getTemplates( \IPS\cms\Templates::RETURN_DATABASE + \IPS\cms\Templates::RETURN_DATABASE_AND_IN_DEV ) as $template )
        {
           
$title = \IPS\cms\Templates::readableGroupName( $template->group );

            switch(
$template->original_group )
            {
                case
'listing':
                   
$templatesList[ $template->group ] = $title;
                    break;
                case
'display':
                   
$templatesDisplay[ $template->group ] = $title;
                    break;
            }
        }

       
$form->add( new \IPS\Helpers\Form\Select( 'category_template_listing', ( ( $this->id and $this->template_listing ) ? $this->template_listing : '0' ), FALSE, array( 'options' => $templatesList ) ) );
       
$form->add( new \IPS\Helpers\Form\Select( 'category_template_display', ( ( $this->id and $this->template_display ) ? $this->template_display : '0' ), FALSE, array( 'options' => $templatesDisplay ) ) );

       
$form->addTab( 'content_content_form_header__meta' );
       
       
$form->add( new \IPS\Helpers\Form\Text( 'category_page_title',  $this->page_title, FALSE, array(), NULL, NULL, NULL ) );
       
$form->add( new \IPS\Helpers\Form\TextArea( 'category_meta_keywords', $this->meta_keywords, FALSE, array(), NULL, NULL, NULL, 'category_meta_keywords' ) );
       
$form->add( new \IPS\Helpers\Form\TextArea( 'category_meta_description', $this->meta_description, FALSE, array(), NULL, NULL, NULL, 'category_meta_description' ) );

        if ( \
IPS\Application::appIsEnabled( 'forums' ) )
        {
           
$form->addTab( 'content_content_form_tab__forum' );
           
           
$form->add( new \IPS\Helpers\Form\YesNo( 'category_forum_override', ( $this->id ? $this->forum_override : NULL ), FALSE, array(
                   
'togglesOn' => array(
                       
'database_forum_record',
                       
'database_forum_comments',
                       
'database_forum_forum',
                       
'database_forum_prefix',
                       
'database_forum_suffix',
                       
'database_forum_delete'    
                   
)
            ),
NULL, NULL, NULL, 'category_forum_override' ) );
           
           
$form->add( new \IPS\Helpers\Form\YesNo( 'database_forum_record', $this->id ? $this->forum_record : FALSE, FALSE, array( 'togglesOn' => array(
                   
'database_forum_comments',
                   
'database_forum_forum',
                   
'database_forum_prefix',
                   
'database_forum_suffix',
                   
'database_forum_delete'
           
) ), NULL, NULL, NULL, 'database_forum_record' ) );
               
           
$form->add( new \IPS\Helpers\Form\YesNo( 'database_forum_comments', $this->id ? $this->forum_comments : FALSE, FALSE, array(), NULL, NULL, NULL, 'database_forum_comments' ) );

           
$form->add( new \IPS\Helpers\Form\Node( 'database_forum_forum', $this->id ? $this->forum_forum : NULL, FALSE, array(
                   
'class'              => '\IPS\forums\Forum',
                   
'disabled'          => false,
                   
'permissionCheck' => function( $node )
                    {
                        return
$node->sub_can_post;
                    }
            ), function(
$val )
            {
                if ( !
$val and \IPS\Request::i()->category_forum_override and \IPS\Request::i()->database_forum_record_checkbox )
                {
                    throw new \
InvalidArgumentException('cms_database_no_forum_selected');
                }
                return
true;
            },
NULL, NULL, NULL, 'database_forum_forum' ) );
               
           
$form->add( new \IPS\Helpers\Form\Text( 'database_forum_prefix',  $this->id ? $this->forum_prefix: '', FALSE, array( 'trim' => FALSE ), NULL, NULL, NULL, 'database_forum_prefix' ) );
           
$form->add( new \IPS\Helpers\Form\Text( 'database_forum_suffix',  $this->id ? $this->forum_suffix: '', FALSE, array( 'trim' => FALSE ), NULL, NULL, NULL, 'database_forum_suffix' ) );
           
$form->add( new \IPS\Helpers\Form\YesNo( 'database_forum_delete', $this->id ? $this->forum_delete : FALSE, FALSE, array(), NULL, NULL, NULL, 'database_forum_delete' ) );
        }
       
       
$form->addTab( 'content_content_form_header__fields' );
       
       
$cats        = $this->id ? ( $this->fields === NULL ? '*' : $this->fields ) : '*';
       
$options    = array();
       
$fieldClass    = 'IPS\cms\Fields' . \IPS\Request::i()->database_id;
       
$database    = \IPS\cms\Databases::load( \IPS\Request::i()->database_id );

        foreach(
$fieldClass::data( NULL, NULL, $fieldClass::FIELD_SKIP_TITLE_CONTENT ) as $field )
        {
            if (
$field->id !== $database->field_title AND $field->id !== $database->field_content )
            {
               
$options[ $field->id ] = $field->_title;
            }        
        }

        if (
count( $options ) )
        {
           
$form->add( new \IPS\Helpers\Form\Select( 'category_fields', $cats, FALSE, array(
               
'multiple'  => true,
               
'unlimited' => '*',
               
'options'   => $options,
            ),
NULL, NULL, NULL, 'category_fields' ) );
        }
    }
   
   
/**
     * [Node] Format form values from add/edit form for save
     *
     * @param    array    $values    Values from the form
     * @return    array
     */
   
public function formatFormValues( $values )
    {
        if ( !
$this->database_id )
        {
            if ( isset(
$values['category_database_id'] ) )
            {
               
$this->database_id = $values['category_database_id'];
            }
            else if ( isset( \
IPS\Request::i()->database_id ) )
            {
               
$this->database_id = \IPS\Request::i()->database_id;
            }

           
$values['category_database_id'] = $this->database_id;
        }

        if ( !
$this->id )
        {
           
$this->_updatePaths = TRUE;
           
$this->save();
        }

        if ( isset(
$values['category_name'] ) AND is_array( $values['category_name'] ) )
        {
           
$name = $values['category_name'][ \IPS\Lang::defaultLanguage() ];
        }
        else if( isset(
$values['category_name'] ) )
        {
           
$name = $values['category_name'];
        }

       
/* Save the name and description */
       
if( isset( $values['category_name'] ) )
        {
            \
IPS\Lang::saveCustom( 'cms', 'content_cat_name_' . $this->id, $values['category_name'] );
        }

        if( isset(
$values['category_description'] ) )
        {
            \
IPS\Lang::saveCustom( 'cms', 'content_cat_name_' . $this->id . '_desc', $values['category_description'] );
            unset(
$values['category_description'] );
        }
       
        if ( isset(
$name ) AND empty( $values['category_furl_name'] ) )
        {
           
$ok  = FALSE;
       
            try
            {
               
$cat = \IPS\cms\Categories::load( \IPS\Http\Url\Friendly::seoTitle( $name ), 'category_furl_name' );
               
               
/* We have a cat, is it in a different database? */
               
if ( $cat->database_id != \IPS\Request::i()->database_id )
                {
                   
$ok = TRUE;
                }
                   
                if ( isset( \
IPS\Request::i()->id ) )
                {
                    if (
$cat->id == \IPS\Request::i()->id )
                    {
                       
$ok = TRUE;
                    }
                }
            }
            catch ( \
OutOfRangeException $e )
            {
               
$ok = TRUE;
            }

            if ( \
IPS\Request::i()->category_parent_id == 0 and \IPS\cms\Categories::isFurlCollision( \IPS\Http\Url\Friendly::seoTitle( $name ) ) )
            {
               
$ok = FALSE;
            }

            if (
$ok === TRUE )
            {
               
$values['furl_name'] = \IPS\Http\Url\Friendly::seoTitle( $name );
            }
            else
            {
               
$values['furl_name'] = $this->id . '_' . \IPS\Http\Url\Friendly::seoTitle( $name );
            }
        }
        else if( isset(
$values['category_furl_name'] ) )
        {
           
$values['furl_name'] = \IPS\Http\Url\Friendly::seoTitle( $values['category_furl_name'] );
        }

        if(
array_key_exists( 'category_furl_name_choice', $values ) )
        {
            unset(
$values['category_furl_name_choice'] );
        }
       
        if ( isset(
$values['category_parent_id'] ) AND ( ! empty( $values['category_parent_id'] ) OR $values['category_parent_id'] === 0 ) )
        {
           
$values['category_parent_id'] = ( $values['category_parent_id'] === 0 ) ? 0 : $values['category_parent_id']->id;
        }

        if (
$this->furl_name !== $values['furl_name'] or $this->parent_id !== $values['category_parent_id'] )
        {
           
$this->_updatePaths = TRUE;
        }

        if ( isset(
$values['category_template_listing'] ) )
        {
           
$values['category_template_listing'] = ( $values['category_template_listing'] !== '_none_' ) ? $values['category_template_listing'] : NULL;
        }

        if ( isset(
$values['category_template_display'] ) )
        {
           
$values['category_template_display'] = ( $values['category_template_display'] !== '_none_' ) ? $values['category_template_display'] : NULL;
        }

        if ( \
IPS\Application::appIsEnabled( 'forums' ) )
        {
           
$values['forum_override'] = isset( $values['category_forum_override'] ) ? $values['category_forum_override'] : 0;
            unset(
$values['category_forum_override'] );
           
            foreach( array(
'forum_record', 'forum_comments', 'forum_prefix', 'forum_suffix', 'forum_delete' ) as $field )
            {
                if (
array_key_exists( 'database_' . $field, $values ) )
                {
                   
$values[ $field ] = $values[ 'database_' . $field ];
                    unset(
$values[ 'database_' . $field ] );
                }
            }

            if ( isset(
$values['database_forum_forum'] ) )
            {
               
$values['forum_forum'] = ( !$values['database_forum_forum'] ) ? 0 : $values['database_forum_forum']->id;
                unset(
$values['database_forum_forum'] );
            }
        }

       
$values['category_allow_rating']    = ( isset( $values['category_allow_rating'] ) ) ? (int) $values['category_allow_rating'] : 0;
       
$values['category_has_perms']        = ( isset( $values['category_has_perms'] ) ) ? (int) $values['category_has_perms'] : 0;
       
$values['forum_record']                = ( isset( $values['forum_record'] ) ) ? (int) $values['forum_record'] : 0;
       
$values['forum_comments']            = ( isset( $values['forum_comments'] ) ) ? (int) $values['forum_comments'] : 0;
       
$values['forum_delete']                = ( isset( $values['forum_delete'] ) ) ? (int) $values['forum_delete'] : 0;

        return
$values;
    }

   
/**
     * [Node] Perform actions after saving the form
     *
     * @param    array    $values    Values from the form
     * @return    void
     */
   
public function postSaveForm( $values )
    {
       
$this->save();

       
/* Clone permissions from the database */
       
if ( ! $this->has_perms )
        {
           
$this->cloneDatabasePermissions();
        }

        if (
$this->_updatePaths )
        {
           
$this->setFullPath();
        }
    }

   
/**
     * Clone permissions from the parent database
     *
     * @return void
     */
   
public function cloneDatabasePermissions()
    {
       
$catPerms = $this->permissions(); /* Called to ensure it has a perm row */
       
$dbPerms  = \IPS\cms\Databases::load( $this->database_id )->permissions();

       
$this->_permissions = array_merge( $dbPerms, array( 'perm_id' => $catPerms['perm_id'], 'perm_type_id' => $this->_id, 'perm_type' => 'categories' ) );
        \
IPS\Db::i()->update( 'core_permission_index', $this->_permissions, array( 'perm_id=?', $catPerms['perm_id'] ) );

       
/* Update tags permission cache */
       
if ( isset( static::$permissionMap['read'] ) )
        {
            \
IPS\Db::i()->update( 'core_tags_perms', array( 'tag_perm_text' => $dbPerms[ 'perm_' . static::$permissionMap['read'] ] ), array( 'tag_perm_aap_lookup=?', md5( static::$permApp . ';' . static::$permType . ';' . $this->_id ) ) );
        }
    }
   
   
/**
     * [Node] Does the currently logged in user have permission to delete this node?
     *
     * @return    bool
     */
   
public function canDelete()
    {
       
$database = \IPS\cms\Databases::load( $this->database_id );
        if ( !
$database->cat_index_type and $database->numberOfCategories() <= 1 )
        {
            return
FALSE;
        }
        return
parent::canDelete();
    }

   
/**
     *  Delete
     *
     * @return void
     */
   
public function delete()
    {
       
/* Remove tags, if any */
       
$aap = md5( 'content;categories;' . $this->id );

        \
IPS\Db::i()->delete( 'core_tags', array( 'tag_aap_lookup=?', $aap ) );
        \
IPS\Db::i()->delete( 'core_tags_perms', array( 'tag_perm_aap_lookup=?', $aap ) );

       
/* Delete category follows */
       
\IPS\Db::i()->delete( 'core_follow', array( "follow_app=? AND follow_area=? AND follow_rel_id=?", static::$permApp, static::$permType . $this->database()->id, $this->_id ) );

        if(
$this->hasChildren() )
        {
            foreach (
$this->children( NULL, NULL, TRUE ) as $child )
            {
               
$child->delete();
            }
        }

       
parent::delete();
    }

   
/**
     * @brief    Cached URL
     */
   
protected $_url    = NULL;

   
/**
     * @brief    Cached title
     */
   
protected $_catTitle = NULL;
   
   
/**
     * @brief    Cached title for strip tags version
     */
   
protected $_catTitleLangKey = NULL;
   
   
/**
     * @brief    Last comment time
     */
   
protected $_lastCommentTime = FALSE;

   
/**
     * @brief   Permissions mashed up with db
     */
   
protected $_permsMashed = FALSE;

   
/**
     * @brief   FURL changed
     */
   
protected $_updatePaths = FALSE;
   
   
/**
     * Disabled permissions
     * Allow node classes to define permissions that are unselectable in the permission matrix
     *
     * @return array    array( {group_id} => array( 'read', 'view', 'perm_7' );
     */
   
public function disabledPermissions()
    {
       
$database  = \IPS\cms\Databases::load( $this->database_id );
       
$dbPerms   = $database->permissions();
       
$disabled  = array();
       
        foreach( array(
'view', 2, 3, 4, 5, 6, 7 ) as $perm )
        {
           
/* Remove unticked database permissions */
           
if ( $dbPerms['perm_' . $perm ] != '*' )
            {
               
$db = explode( ',', $dbPerms['perm_' . $perm ] );
               
                foreach ( \
IPS\Member\Group::groups() as $group )
                {
                    if ( !
in_array( $group->g_id, $db ) )
                    {
                       
$disabled[ $group->g_id ][] = $perm;
                    }
                }
            }
        }

        try
        {
           
$guestGroup = \IPS\Member\Group::load( \IPS\Settings::i()->guest_group );
        }
        catch( \
OutOfRangeException $e )
        {
            throw new \
UnderflowException( 'invalid_guestgroup_admin', 199 );
        }

        if( !
$this->can_view_others )
        {
           
$disabled[ $guestGroup->g_id ] = array( 'view', 2, 3, 4, 5, 6, 7 );
        }

        return
$disabled;
    }
   
   
/**
     * Get permissions
     *
     * @return    array
     */
   
public function permissions()
    {
       
/* Let the ACP/Setup use normal permissions or it'll get messy */
       
if ( !\IPS\Dispatcher::hasInstance() OR \IPS\Dispatcher::i()->controllerLocation !== 'front' )
        {
           
$this->_permsMashed = true;
            return
parent::permissions();
        }

        if ( !
$this->_permsMashed )
        {
           
/* Make sure we have perms */
           
if ( ! $this->_permissions )
            {
               
parent::permissions();
            }

           
$database  = \IPS\cms\Databases::load( $this->database_id );
           
$dbPerms   = $database->permissions();
           
$savePerms = $this->_permissions;

            foreach( array(
'view', 2, 3, 4, 5, 6, 7 ) as $perm )
            {
               
/* Make sure category permission cannot be better than database permissions */
               
if ( $dbPerms['perm_' . $perm ] != $savePerms['perm_' . $perm ] )
                {
                   
/* Category using *? Use database instead */
                   
if ( $savePerms['perm_' . $perm ] == '*' )
                    {
                       
$savePerms['perm_' . $perm ] = $dbPerms['perm_' . $perm ];
                    }
                    else if (
$dbPerms['perm_' . $perm ] == '*' )
                    {
                       
/* That's fine, cat is going to be less permissive than * */
                       
continue;
                    }
                    else
                    {
                       
/* Make sure that groups not in the database are not in here too */
                       
$db  = explode( ',', $dbPerms['perm_' . $perm ] );
                       
$cat = explode( ',', $savePerms['perm_' . $perm ] );

                       
$savePerms['perm_' . $perm ] = implode( ',', array_intersect( $db, $cat ) );
                    }
                }
            }

           
$savePerms['perm_2'] = $this->readPermissionMergeWithPage( $savePerms );

           
$this->_permissions = $savePerms;
           
$this->_permsMashed = TRUE;
        }

        return
$this->_permissions;
    }

   
/**
     * Return least favourable permissions based on category and page
     *
     * @param   array   $perms      Array of perms
     *
     * @return string
     */
   
public function readPermissionMergeWithPage( $perms=NULL )
    {
       
$database = \IPS\cms\Databases::load( $this->database_id );

       
/* Now check against the page */
       
if ( $database->page_id )
        {
            try
            {
               
$page      = \IPS\cms\Pages\Page::load( $database->page_id );
               
$pagePerms = $page->permissions();
               
$catPerms  = ( $perms ) ? $perms : $this->permissions();

                if (
$pagePerms['perm_view'] === '*' )
                {
                    return
$catPerms['perm_2'];
                }
                else if (
$catPerms['perm_2'] === '*' )
                {
                    return
$pagePerms['perm_view'];
                }
                else
                {
                    return
implode( ',', array_intersect( explode( ',', $pagePerms['perm_view'] ), explode( ',', $catPerms['perm_2'] ) ) );
                }
            }
            catch ( \
OutOfRangeException $ex )
            {

            }
        }

        return
$this->_permissions['perm_2'];
    }

   
/**
     * Get URL
     *
     * @return    \IPS\Http\Url|NULL
     */
   
public function url()
    {
        if(
$this->_url === NULL )
        {
            if ( \
IPS\cms\Pages\Page::$currentPage and \IPS\cms\Databases::load( $this->database_id )->page_id == \IPS\cms\Pages\Page::$currentPage->id )
            {
               
$pagePath = \IPS\cms\Pages\Page::$currentPage->full_path;
            }
            else
            {
                try
                {
                   
$pagePath = \IPS\cms\Pages\Page::loadByDatabaseId( $this->database_id )->full_path;
                }
                catch( \
OutOfRangeException $e )
                {
                    return
NULL;
                }
            }
           
           
$catPath  = $this->full_path;

            if ( \
IPS\cms\Databases::load( $this->database_id )->use_categories )
            {
               
$this->_url = \IPS\Http\Url::internal( "app=cms&module=pages&controller=page&path=" . $pagePath . '/' . $catPath, 'front', 'content_page_path', $this->furl_name );
            }
            else
            {
               
$this->_url = \IPS\Http\Url::internal( "app=cms&module=pages&controller=page&path=" . $pagePath, 'front', 'content_page_path', $this->furl_name );
            }
        }

        return
$this->_url;
    }
   
   
/**
     * Get URL from index data
     *
     * @param    array        $indexData        Data from the search index
     * @param    array        $itemData        Basic data about the item. Only includes columns returned by item::basicDataColumns()
     * @param    array|NULL    $containerData    Basic data about the container. Only includes columns returned by container::basicDataColumns()
     * @return    \IPS\Http\Url
     */
   
public static function urlFromIndexData( $indexData, $itemData, $containerData )
    {
       
$recordClass = $indexData['index_class'];
        if (
in_array( 'IPS\Content\Comment', class_parents( $recordClass ) ) )
        {
           
$recordClass = $recordClass::$itemClass;
        }
        if (
$recordClass::$pagePath === NULL )
        {
           
$recordClass::$pagePath = \IPS\Db::i()->select( array( 'page_full_path' ), 'cms_pages', array( 'page_id=?', $recordClass::database()->page_id ) )->first();
        }
               
        if (
$recordClass::database()->use_categories )
        {
            return \
IPS\Http\Url::internal( "app=cms&module=pages&controller=page&path=" . $recordClass::$pagePath . '/' . $itemData['extra'], 'front', 'content_page_path', $itemData['extra'] );
        }
        else
        {
            return \
IPS\Http\Url::internal( "app=cms&module=pages&controller=page&path=" . $recordClass::$pagePath, 'front', 'content_page_path', '' );
        }
    }
   
   
/**
     * Get title from index data
     *
     * @param    array        $indexData        Data from the search index
     * @param    array        $itemData        Basic data about the item. Only includes columns returned by item::basicDataColumns()
     * @param    array|NULL    $containerData    Basic data about the author. Only includes columns returned by container::basicDataColumns()
     * @return    \IPS\Http\Url
     */
   
public static function titleFromIndexData( $indexData, $itemData, $containerData )
    {
       
$recordClass = $indexData['index_class'];
        if (
in_array( 'IPS\Content\Comment', class_parents( $recordClass ) ) )
        {
           
$recordClass = $recordClass::$itemClass;
        }
       
        if (
$recordClass::database()->use_categories )
        {
            return \
IPS\Member::loggedIn()->language()->addToStack( static::$titleLangPrefix . $indexData['index_container_id'] );
        }
        else
        {
            return
$recordClass::database()->_title;
        }
    }
   
   
/**
     * Get Page Title for use in <title> tag
     *
     * @return    string
     */
   
public function pageTitle()
    {
        if (
$this->page_title )
        {
            return
$this->page_title;
        }
       
        return
$this->database()->pageTitle();
    }

   
/**
     * Get title of category
     *
     * @return    string
     */
   
protected function get__title()
    {
       
/* If the DB is in a page, and we're not using categories, then return the page title, not the category title for continuity */
       
if ( ! \IPS\cms\Databases::load( $this->database_id )->use_categories )
        {
            if ( !
$this->_catTitle )
            {
                try
                {
                   
$page = \IPS\cms\Pages\Page::loadByDatabaseId( $this->database_id );
                   
$this->_catTitle = $page->_title;
                }
                catch( \
OutOfRangeException $e )
                {
                   
$this->_catTitle = parent::get__title();
                }
            }

            return
$this->_catTitle;
        }
        else
        {
            return
parent::get__title();
        }
    }
   
   
/**
     * [Node] Get Title language key, not added to a language stack
     *
     * @return    string|null
     */
   
protected function get__titleLanguageKey()
    {
       
/* If the DB is in a page, and we're not using categories, then return the page title, not the category title for continuity */
       
if ( ! \IPS\cms\Databases::load( $this->database_id )->use_categories )
        {
            if ( !
$this->_catTitleLangKey )
            {
                try
                {
                   
$page = \IPS\cms\Pages\Page::loadByDatabaseId( $this->database_id );
                   
$this->_catTitleLangKey = $page->_titleLanguageKey;
                }
                catch( \
OutOfRangeException $e )
                {
                   
$this->_catTitleLangKey = parent::get__titleLanguageKey();
                }
            }

            return
$this->_catTitleLangKey;
        }
        else
        {
            return
parent::get__titleLanguageKey();
        }
    }

   
/**
     * [Node] Get Description
     *
     * @return    string|null
     */
   
protected function get__description()
    {
        if ( ! static::
database()->use_categories )
        {
            return static::
database()->_description;
        }

        return ( \
IPS\Member::loggedIn()->language()->addToStack('content_cat_name_' . $this->id . '_desc') === 'content_cat_name_' . $this->id . '_desc' ) ? $this->description : \IPS\Member::loggedIn()->language()->addToStack('content_cat_name_' . $this->id . '_desc');
    }

   
/**
     * Get number of items
     *
     * @return    int
     */
   
protected function get__items()
    {
        if ( !
$this->can_view_others and !\IPS\Member::loggedIn()->modPermission( 'can_content_view_others_records' ) )
        {
            return \
IPS\Db::i()->select('count(*)', 'cms_custom_database_' . $this->database_id, array("record_future_date=0 AND category_id=? AND record_approved=1 AND member_id=?", $this->id, \IPS\Member::loggedIn()->member_id) )->first();
        }
       
        return (int)
$this->records;
    }
   
   
/**
     * Set number of items
     *
     * @param    int    $val    Items
     * @return    int
     */
   
protected function set__items( $val )
    {
       
$this->records = (int) $val;
    }

   
/**
     * Get number of reviews
     *
     * @return    int
     */
   
protected function get__reviews()
    {
        return (int)
$this->record_reviews;
    }

   
/**
     * Set number of reviews
     *
     * @param    int    $val    Comments
     * @return    int
     */
   
protected function set__reviews( $val )
    {
       
$this->record_reviews = (int) $val;
    }

   
/**
     * Get number of unapproved reviews
     *
     * @return    int
     */
   
protected function get__unapprovedReviews()
    {
        return (int)
$this->record_reviews_queued;
    }

   
/**
     * Set number of unapproved reviews
     *
     * @param    int    $val    Comments
     * @return    int
     */
   
protected function set__unapprovedReviews( $val )
    {
       
$this->record_reviews_queued = (int) $val;
    }

   
/**
     * Get number of comments
     *
     * @return    int
     */
   
protected function get__comments()
    {
        return (int)
$this->record_comments;
    }
   
   
/**
     * Set number of items
     *
     * @param    int    $val    Comments
     * @return    int
     */
   
protected function set__comments( $val )
    {
       
$this->record_comments = (int) $val;
    }
   
   
/**
     * [Node] Get number of unapproved content items
     *
     * @return    int
     */
   
protected function get__unapprovedItems()
    {
        return
$this->records_queued;
    }
   
   
/**
     * [Node] Get number of unapproved content comments
     *
     * @return    int
     */
   
protected function get__unapprovedComments()
    {
        return
$this->record_comments_queued;
    }
   
   
/**
     * [Node] Get number of unapproved content items
     *
     * @param    int    $val    Unapproved Items
     * @return    void
     */
   
protected function set__unapprovedItems( $val )
    {
       
$this->records_queued = $val;
    }

   
/**
     * [Node] Get number of future publishing items
     *
     * @return    int
     */
   
protected function get__futureItems()
    {
        return
$this->records_future;
    }

   
/**
     * [Node] Get number of future content items
     *
     * @param    int    $val    Future Items
     * @return    void
     */
   
protected function set__futureItems( $val )
    {
       
$this->records_future = $val;
    }
   
   
/**
     * [Node] Get number of unapproved content comments
     *
     * @param    int    $val    Unapproved Comments
     * @return    void
     */
   
protected function set__unapprovedComments( $val )
    {
       
$this->record_comments_queued = $val;
    }

   
/**
     * Get the template listing template
     *
     * @return  string      Templateg group
     */
   
public function get__template_listing()
    {
        if (
$this->template_listing AND static::database()->use_categories )
        {
            return
$this->template_listing;
        }

        return static::
database()->template_listing;
    }

   
/**
     * Get the template display template
     *
     * @return  string      Templateg group
     */
   
public function get__template_display()
    {
        if (
$this->template_display AND static::database()->use_categories )
        {
            return
$this->template_display;
        }

        return static::
database()->template_display;
    }
   
   
/**
     * Set last comment
     *
     * @param    \IPS\Content\Comment    $comment    The latest comment or NULL to work it out
     * @return    void
     */
   
public function setLastComment( \IPS\Content\Comment $comment=NULL )
    {
       
$database = \IPS\cms\Databases::load( $this->database_id );

       
/* Make sure it wasn't a comment added to a hidden record */
       
if ( $comment !== NULL )
        {
            if (
$comment->item()->hidden() OR $comment->item()->isFutureDate() )
            {
               
$comment = NULL;
            }
        }

        if (
$comment === NULL )
        {  
            try
            {
               
$recordClass  = '\IPS\cms\Records' . $this->database_id;
               
$commentClass = '\IPS\cms\Records\Comment' . $this->database_id;
               
$comment      = NULL;

                if ( static::
$latestRecordAdded === NULL )
                {
                    static::
$latestRecordAdded = $recordClass::constructFromData(
                        \
IPS\Db::i()->select(
                           
'*',
                           
'cms_custom_database_' . $this->database_id,
                            array(
'category_id=? AND record_approved=1 AND record_future_date=0', $this->id ),
                           
'record_last_comment DESC',
                            array(
0, 1 ),
                           
NULL,
                           
NULL,
                            \
IPS\Db::SELECT_FROM_WRITE_SERVER
                       
)->first()
                    );
                }

                if ( static::
$latestRecordAdded->record_comments )
                {
                    if ( static::
$latestRecordAdded->record_comments AND ( $database->_comment_bump & \IPS\cms\Databases::BUMP_ON_COMMENT ) )
                    {
                        if ( static::
$latestRecordAdded->useForumComments() )
                        {
                           
$syncRecord = static::$latestRecordAdded;
                           
                            try
                            {
                               
$comment = $syncRecord->comments( 1, 0, 'date', 'desc', NULL, FALSE );
                            }
                            catch( \
Exception $e ) { }
                        }
                        else
                        {
                            try
                            {
                               
$comment = $commentClass::constructFromData( \IPS\Db::i()->select( '*', 'cms_database_comments', array( 'comment_record_id=? AND comment_approved=1', \IPS\Db::i()->select( 'primary_id_field', 'cms_custom_database_' . $this->database_id, array( 'category_id=? AND record_approved=1', $this->id ), 'record_last_comment DESC', 1 )->first() ), 'comment_date DESC', 1, NULL, NULL, \IPS\Db::SELECT_FROM_WRITE_SERVER )->first() );
                            }
                            catch( \
UnderflowException $e ) { }
                        }
                    }

                    if (
$comment and ( static::$latestRecordAdded->record_last_comment > $comment->mapped('date') ) )
                    {
                       
$comment = NULL;

                    }
                }
            }
            catch ( \
UnderflowException $e )
            {
               
$this->last_record_date   = 0;
               
$this->last_record_member = 0;
               
$this->last_record_name = '';
               
$this->last_title = NULL;
               
$this->last_record_id = 0;
                return;
            }
        }

        if (
$comment !== NULL and ( $database->_comment_bump & \IPS\cms\Databases::BUMP_ON_COMMENT ) )
        {
           
$this->last_record_date     = $comment->mapped('date');
           
$this->last_record_member   = intval( $comment->author()->member_id );
           
$this->last_record_name     = $comment->author()->member_id ? $comment->author()->name : NULL;
           
$this->last_record_seo_name = \IPS\Http\Url\Friendly::seoTitle( $this->last_poster_name );
           
$this->last_title           = mb_substr( $comment->item()->mapped('title'), 0, 255 );
           
$this->last_seo_title       = \IPS\Http\Url\Friendly::seoTitle( $this->last_title );
           
$this->last_record_id       = $comment->item()->_id;
        }
        else if ( static::
$latestRecordAdded !== NULL )
        {
           
$this->last_record_date     = static::$latestRecordAdded->record_saved;
           
$this->last_record_member   = static::$latestRecordAdded->member_id;
           
$this->last_record_name     = static::$latestRecordAdded->member_id ? static::$latestRecordAdded->record_last_comment_name : NULL;
           
$this->last_title           = mb_substr( static::$latestRecordAdded->_title, 0, 255 );
           
$this->last_seo_title       = \IPS\Http\Url\Friendly::seoTitle( mb_substr( static::$latestRecordAdded->_title, 0, 255 ) );
           
$this->last_record_id       = static::$latestRecordAdded->_id;
        }
       
       
$this->records        = \IPS\Db::i()->select( 'COUNT(*)', 'cms_custom_database_' . $this->database_id, array( 'record_approved=1 AND record_future_date=0 AND category_id=?', $this->id ), NULL, NULL, NULL, NULL, \IPS\Db::SELECT_FROM_WRITE_SERVER )->first();
       
$this->records_queued = \IPS\Db::i()->select( 'COUNT(*)', 'cms_custom_database_' . $this->database_id, array( 'record_approved=0 AND category_id=?', $this->id ), NULL, NULL, NULL, NULL, \IPS\Db::SELECT_FROM_WRITE_SERVER )->first();
       
$this->records_future = \IPS\Db::i()->select( 'COUNT(*)', 'cms_custom_database_' . $this->database_id, array( 'record_future_date=1 AND category_id=?', $this->id ), NULL, NULL, NULL, NULL, \IPS\Db::SELECT_FROM_WRITE_SERVER )->first();

        static::
$latestRecordAdded = NULL;
    }

   
/**
     * Get last comment time
     *
     * @note    This should return the last comment time for this node only, not for children nodes
     * @return    \IPS\DateTime|NULL
     */
   
public function getLastCommentTime()
    {
        if( !
$this->can_view_others and !\IPS\Member::loggedIn()->modPermission( 'can_content_view_others_records' ) )
        {
            try
            {
               
$select = \IPS\Db::i()->select('record_last_comment', 'cms_custom_database_' . $this->database_id, array("record_future_date=0 AND category_id=? AND record_approved=1 AND member_id=?", $this->id, \IPS\Member::loggedIn()->member_id), 'record_last_comment DESC', 1 )->first();
            }
            catch ( \
UnderflowException $e )
            {
                return
NULL;
            }

            return
$select ?  \IPS\DateTime::ts( $select ) : NULL;
        }

        return
$this->last_record_date ? \IPS\DateTime::ts( $this->last_record_date ) : NULL;
    }
   
   
/**
     * Get last post data
     *
     * @return    array|NULL
     */
   
public function lastPost()
    {
       
$result = NULL;
       
$RecordsClass = static::$contentItemClass;

       
/* This category does not allow you to see records from other users... */
       
if( $this->can_view_others or \IPS\Member::loggedIn()->modPermission( 'can_content_view_others_records' ) )
        {
            if (
$this->last_record_date )
            {
                try
                {
                   
$result = array( 'author' => \IPS\Member::load( $this->last_record_member ), 'record_url' => $RecordsClass::load( $this->last_record_id )->url(), 'record_title' => $this->last_title, 'date' => $this->last_record_date );

                    if ( !
$this->last_record_member AND $this->last_record_name )
                    {
                       
$result[ 'author' ]->name = $this->last_record_name;
                    }
                }
                catch ( \
OutOfRangeException $e )
                {
                }
            }
        }
        else
        {
            try
            {
               
$record = $RecordsClass::constructFromData( \IPS\Db::i()->select('*', 'cms_custom_database_' . $this->database_id, array("record_future_date=0 AND category_id=? AND record_approved=1 AND member_id=?", $this->id, \IPS\Member::loggedIn()->member_id), 'record_last_comment DESC', 1 )->first() );
               
$result = array( 'author' => $record->author(), 'record_url' => $record->url(), 'record_title' => $record->_title, 'date' => $record->record_last_comment );
            }
            catch ( \
Exception $e )
            {
            }
        }

        foreach(
$this->children() as $child )
        {
           
$childLastPost = $child->lastPost();

            if( !
$result OR $childLastPost['date'] > $result['date'] )
            {
               
$result = $childLastPost;
            }
        }

        return
$result;
    }

   
/**
     * Resets a folder path
     *
     * @return    void
     */
   
public function setFullPath()
    {
       
$this->full_path = $this->furl_name;

        if (
$this->parent_id )
        {
           
$parentId = $this->parent_id;
           
$failSafe = 0;
           
$path     = array();

            while(
$parentId != 0 )
            {
                if (
$failSafe > 50 )
                {
                    break;
                }

                try
                {
                   
$parent = static::load( $parentId );

                    if ( !
$parent->furl_name )
                    {
                       
$parent->furl_name = \IPS\Http\Url\Friendly::seoTitle( $parent->name );
                    }

                   
$parentId = $parent->parent_id;
                   
$path[]   = $parent->furl_name;
                }
                catch( \
OutOfRangeException $e )
                {
                    break;
                }

               
$failSafe++;
            }

           
krsort( $path );
           
$path[] = $this->furl_name;

           
$this->full_path = trim( implode( '/', $path ), '/' );
        }

       
$this->save();

        foreach (
$this->children( NULL ) as $child )
        {
           
$child->setFullPath();
        }
    }
   
   
/**
     * [Node] Does the currently logged in user have permission to edit permissions for this node?
     *
     * @return    bool
     */
   
public function canManagePermissions()
    {
       
$can = parent::canManagePermissions();
       
        return (
$can === FALSE ) ? FALSE : (boolean) $this->has_perms;
    }
   
   
/**
     * Get which permission keys can access all records in a category which
     * can normally only show records to the author
     *
     * @return    array
     */
   
public function permissionsThatCanAccessAllRecords()
    {
       
$normal        = $this->searchIndexPermissions();
       
$return        = array();
       
$members    = array();
       
        foreach ( \
IPS\Db::i()->select( '*', 'core_moderators' ) as $moderator )
        {
            if (
$moderator['perms'] === '*' or in_array( 'can_content_view_others_records', explode( ',', $moderator['perms'] ) ) )
            {
                if(
$moderator['type'] === 'g' )
                {
                   
$return[] = $moderator['id'];
                }
                else
                {
                   
$members[] = "m{$moderator['id']}";
                }
            }
        }
       
       
$return = ( $normal == '*' ) ? array_unique( $return ) : array_intersect( explode( ',', $normal ), array_unique( $return ) );
   
        if(
count( $members ) )
        {
           
$return = array_merge( $return, $members );
        }
       
        return
$return;
    }
   
   
/**
     * Update search index permissions
     *
     * @return  void
     */
   
protected function updateSearchIndexPermissions()
    {
        if (
$this->can_view_others )
        {
            return
parent::updateSearchIndexPermissions();
        }
        else
        {
           
$permissions = implode( ',', $this->permissionsThatCanAccessAllRecords() );
            \
IPS\Content\Search\Index::i()->massUpdate( 'IPS\cms\Records' . static::database()->_id, $this->_id, NULL, $permissions, NULL, NULL, NULL, NULL, NULL, TRUE );
            \
IPS\Content\Search\Index::i()->massUpdate( 'IPS\cms\Records\Comment' . static::database()->_id, $this->_id, NULL, $permissions, NULL, NULL, NULL, NULL, NULL, TRUE );
        }
    }
   
   
/**
     * Get the filter cookie for this category
     *
     * @return array|null
     */
   
public function getFilterCookie()
    {
        if ( isset( \
IPS\Request::i()->cookie['cms_filters'] ) )
        {
           
$saved = json_decode( \IPS\Request::i()->cookie['cms_filters'], TRUE );

            if (
array_key_exists( $this->id, $saved ) and count( $saved[ $this->id ] ) )
            {
                return
$saved[ $this->id ];
            }
        }

        return
NULL;
    }

   
/**
     * Save filter cookie for this category
     *
     * @param   array|FALSE  $values Filter values to save (array) or FALSE to remove cookie
     * @return null
     */
   
public function saveFilterCookie( $values )
    {
       
$cookie = ( isset( \IPS\Request::i()->cookie['cms_filters'] ) ) ? json_decode( \IPS\Request::i()->cookie['cms_filters'], TRUE ) : array();

        if (
$values === FALSE )
        {
            if (
array_key_exists( $this->id, $cookie ) )
            {
                unset(
$cookie[ $this->id ] );
            }
        }
        else
        {
           
/* We only want to include ones where we have actually specified values to filter on */
           
$toSave = array();
            foreach(
$values AS $key => $data )
            {
                if (
count( $data ) )
                {
                   
$toSave[$key] = $data;
                }
            }
           
           
$cookie[ $this->id ] = $toSave;
        }

        \
IPS\Request::i()->setCookie( 'cms_filters', json_encode( $cookie ), \IPS\DateTime::create()->add( new \DateInterval( 'P7D' ) ) );
    }

   
/**
     * [Node] Get meta description
     *
     * @return    string
     */
   
public function metaDescription()
    {
        return
$this->meta_description ?: static::database()->metaDescription();
    }

   
/**
     * [Node] Get meta title
     *
     * @return    string
     */
   
public function metaTitle()
    {
        return
$this->page_title ?: static::database()->metaTitle();
    }

   
/**
     * Check permissions
     *
     * @param    mixed                                $permission        A key which has a value in static::$permissionMap['view'] matching a column ID in core_permission_index
     * @param    \IPS\Member|\IPS\Member\Group|NULL    $member            The member or group to check (NULL for currently logged in member)
     * @return    bool
     * @throws    \OutOfBoundsException    If $permission does not exist in static::$permissionMap
     */
   
public function can( $permission, $member=NULL )
    {
       
$_member = $member ?: \IPS\Member::loggedIn();

        if ( !
$_member->member_id and !$this->can_view_others )
        {
            return
FALSE;
        }

        return
parent::can( $permission, $member );
    }
}