Seditio Source
Root |
./othercms/b2evolution_7.2.3/inc/items/model/_item.class.php
<?php
/**
 * This file implements the Item class.
 *
 * This file is part of the evoCore framework - {@link http://evocore.net/}
 * See also {@link https://github.com/b2evolution/b2evolution}.
 *
 * @license GNU GPL v2 - {@link http://b2evolution.net/about/gnu-gpl-license}
 *
 * @copyright (c)2003-2020 by Francois Planque - {@link http://fplanque.com/}
 * Parts of this file are copyright (c)2004-2006 by Daniel HAHLER - {@link http://thequod.de/contact}.
 *
 * @package evocore
 */
if( !defined('EVO_MAIN_INIT') ) die( 'Please, do not access this page directly.' );

/**
 * Includes:
 */
load_funcs( 'items/model/_item.funcs.php');
load_class( 'slugs/model/_slug.class.php', 'Slug' );
load_class( 'links/model/_linkowner.class.php', 'LinkOwner' );
load_class( 'links/model/_linkitem.class.php', 'LinkItem' );

/**
 * Item Class
 *
 * @package evocore
 */
class Item extends ItemLight
{
   
/**
     * Creation date (timestamp)
     * @var integer
     */
   
var $datecreated;

   
/**
     * The User who has created the Item (lazy-filled).
     * @see Item::get_creator_User()
     * @see Item::set_creator_User()
     * @var User
     * @access protected
     */
   
var $creator_User;


   
/**
     * The User who has edited the Item last time (lazy-filled).
     * @see Item::get_lastedit_User()
     * @var User
     * @access protected
     */
   
var $lastedit_User;


   
/**
     * ID of the user who has edited the Item last time
     * @var integer
     */
   
var $lastedit_user_ID;

   
/**
     * Date when comments or links were added/edited/deleted for this Item last time (timestamp)
     * @see Item::update_last_touched_date()
     * @var integer
     */
   
var $last_touched_ts;

   
/**
     * Date when contents were updated for this Item last time (timestamp)
     * @see Item::update_last_touched_date()
     * @var integer
     */
   
var $contents_last_updated_ts;


   
/**
     * The latest Comment on this Item (lazy-filled).
     * @see Item::get_latest_Comment()
     * @var Comment
     * @access protected
     */
   
var $latest_Comment;


   
/**
     * @deprecated by {@link $creator_User}
     * @var User
     */
   
var $Author;


   
/**
     * ID of the user that created the item
     * @var integer
     */
   
var $creator_user_ID;


   
/**
     * Login of the user that created the item (lazy-filled)
     * @var string
     */
   
var $creator_user_login;


   
/**
     * The assigned User to the item.
     * Can be NULL
     * @see Item::get_assigned_User()
     * @see Item::assign_to()
     *
     * @var User
     * @access protected
     */
   
var $assigned_User;

   
/**
     * ID of the User assigned to the item
     * Can be NULL
     *
     * @var integer
     */
   
var $assigned_user_ID;
   
/**
     * Flag to know if item is assigned to a new user
     * Used to determine if assignment notification should be sent
     * @var boolean
     */
   
var $assigned_to_new_user;

   
/**
     * The visibility status of the item.
     *
     * 'published', 'community', 'deprecated', 'protected', 'private', 'review' or 'draft'
     *
     * @var string
     */
   
var $status;
   
/**
     * Item previous visibility status. It will be set only if the item status was changed.
     * @var string
     */
   
var $previous_status;

   
/**
     * Locale code for the Item content.
     *
     * Examples: en-US, zh-CN-utf-8
     *
     * @var string
     */
   
var $locale;

   
/**
     * Display the Item in list depending on navigation locale
     *
     * 0 - Always show
     * 1 - Show only if matching navigation locale
     *
     * @var integer|boolean
     */
   
var $locale_visibility = 'always';

    var
$content;
   
/**
     * Flag to know if content was updated during current request
     * Used to update excerpt
     * @var boolean
     */
   
var $content_is_updated;

    var
$titletag;

   
/**
     * Lazy filled, use split_page()
     */
   
var $content_pages = NULL;


    var
$wordcount;
   
/**
     * The list of renderers, imploded by '.'.
     * @var string
     * @access protected
     */
   
var $renderers;
   
/**
     * Comments status
     *
     * "open", "disabled" or "closed
     *
     * @var string
     */
   
var $comment_status;

    var
$pst_ID;
    var
$datedeadline = '';
    var
$priority;

   
/**
     * @var array Item orders per category
     */
   
var $orders;
   
/**
     * @var boolean
     */
   
var $featured;

   
/**
     * Have post processing notifications been handled?
     * @var string
     */
   
var $notifications_status;
   
/**
     * Which cron task is responsible for handling notifications?
     * @var integer
     */
   
var $notifications_ctsk_ID;
   
/**
     * What have been notified?
     * Possible values, separated by comma: 'moderators_notified,members_notified,community_notified,pings_sent'
     * @var string
     */
   
var $notifications_flags;

   
/**
     * array of IDs or NULL if we don't know...
     *
     * @var array
     */
   
var $extra_cat_IDs = NULL;

   
/**
     * Has the publish date been explicitly set?
      *
     * @var integer
     */
   
var $dateset = 1;

    var
$priorities;

   
/**
     * @access protected
     * @see Item::get_excerpt()
     * @var string
     */
   
var $excerpt;

   
/**
     * Is the excerpt autogenerated?
     * This will become false when we receive an excerpt that was manually changed in the edit form.
     * @access protected
     * @var boolean
     */
   
var $excerpt_autogenerated = 1;

   
/**
     * Location IDs
     * @var integer
     */
   
var $ctry_ID = NULL;
    var
$rgn_ID = NULL;
    var
$subrg_ID = NULL;
    var
$city_ID = NULL;

   
/**
     * ID of parent Item.
     * @var integer
     */
   
var $parent_ID = NULL;

   
/**
     * Parent Item.
     * @var object
     */
   
var $parent_Item = NULL;

   
/**
     * Item Group ID
     * @var integer
     */
   
var $igrp_ID = NULL;

   
/**
     * Other versions of this Item, linked by igrp_ID
     * @var array
     */
   
var $other_version_items;

   
/**
     * Additional settings for the items.  lazy filled.
      *
     * @see Item::get_setting()
     * @see Item::set_setting()
     * @see Item::load_ItemSettings()
     * Any non vital params should go into there.
     *
     * @var ItemSettings
     */
   
var $ItemSettings;

   
/**
     * Current User read status on this post content ( Only about the post content and not about the post's comments ).
     * This value is not saved into the db. Lazy filled.
     *
     * @var string ( read, new, updated )
     */
   
var $content_read_status = NULL;

   
/**
     * The Type of the Item (lazy filled, use {@link get_ItemType()} to access it.
     * @access protected
     * @var object ItemType
     */
   
var $ItemType;

   
/**
     * Voting result of all votes
     *
     * @var integer
     */
   
var $addvotes;

   
/**
     * A count of all votes
     *
     * @var integer
     */
   
var $countvotes;
   
/**
     * Lazy filled, use {@link get_social_media_image()} to access it.
     */
   
var $social_media_image_File = NULL;

   
/**
     * Current revision
     *
     * @var integer
     */
   
var $revision;

   
/**
     * Cached revisions in order to don't load them twice
     *
     * @var array
     */
   
var $revisions;

   
/**
     * Constructor
     *
     * @param object table Database row
     * @param string
     * @param string
     * @param string
     * @param string for derived classes
     * @param string datetime field name
     * @param string datetime field name
     * @param string User ID field name
     * @param string User ID field name
     */
   
function __construct( $db_row = NULL, $dbtable = 'T_items__item', $dbprefix = 'post_', $dbIDname = 'post_ID', $objtype = 'Item',
                   
$datecreated_field = 'datecreated', $datemodified_field = 'datemodified',
                   
$creator_field = 'creator_user_ID', $lasteditor_field = 'lastedit_user_ID' )
    {
        global
$localtimenow, $default_locale, $current_User;

       
// Call parent constructor:
       
parent::__construct( $db_row, $dbtable, $dbprefix, $dbIDname, $objtype,
                   
$datecreated_field, $datemodified_field,
                   
$creator_field, $lasteditor_field );

        if(
is_null($db_row) )
        {
// New item:
           
global $Collection, $Blog;

            if( isset(
$current_User) )
            {
// use current user as default, if available (which won't be the case during install)
               
$this->creator_user_login = $current_User->login;
               
$this->set_creator_User( $current_User );
            }
           
$this->set( 'dateset', 0 );    // Date not explicitly set yet
           
$this->set( 'notifications_status', 'noreq' );
           
// Set the renderer list to 'default' will trigger all 'opt-out' renderers:
           
$this->set( 'renderers', array('default') );
           
$this->set( 'priority', 3 );
            if( ! empty(
$Blog ) )
            {
// Get default post type from blog setting
               
$default_post_type = $Blog->get_setting('default_post_type');
            }
           
$this->set( 'ityp_ID', ( empty( $default_post_type ) ? 1 /* Post */ : $default_post_type ) );
           
// Set default locale for new item:
           
if( ! empty( $Blog ) )
            {    
// Set locale depending on collection setting:
               
switch( $Blog->get_setting( 'new_item_locale_source' ) )
                {
                    case
'select_coll':
                       
// Use locale of current collection by default:
                       
$new_item_locale = $Blog->get( 'locale' );
                        break;
                    case
'select_user':
                       
// Use locale of current user by default:
                       
if( is_logged_in() )
                        {    
// If current user is logged in
                           
$new_item_locale = $current_User->get( 'locale' );
                        }
                        break;
                }
            }
           
$this->set( 'locale', ( isset( $new_item_locale ) ? $new_item_locale : $default_locale ) );
           
$this->set( 'status', 'draft' );
        }
        else
        {
           
$this->datecreated = $db_row->post_datecreated;           // When Item was created in the system

            // post_last_touched_ts : When Item received last visible change (edit, comment, etc.)
            // Used for:
            //   - Sorting posts if configured this way in collection features.
            // Updated when:
            //   - ANY item field is updated,
            //   - link, unlink an attachment, update an attached file, change a link order
            //   - any child COMMENT of the post is added/updated/deleted,
            //   - link, unlink an attachment, update an attached file, change a link order on any comment
           
$this->last_touched_ts = $db_row->post_last_touched_ts;

           
// post_contents_last_updated_ts : When Item received last content change
            // Used for:
            //   - Knowing if current user has seen the updates on the post
            //   - Sorting forums (by default; can be changed in collection features)
            // Updated only when:
            //   - at least ONE of the fields: title, content, url is updated --> Especially: don't update on status change, workflow change, because it doesn't affect whether users have seen latest content changes or not
            //   - link, unlink an attachment, update an attached file (note: link order changes are not recorded because it doesn't affect whether users have seen lastest content changes)
            //   - a child COMMENT of the post that can be seen in the front-office is added or updated (only Content or Rating fields, or front-office visibility is changed from NOT front-office visibility) (but don't update on deleted comments or invisible comments -- When deleting a comment we actually recompute an OLDER timestamp based on last remaining comment, Also we recompute this when move a front-office visibility latest comment to other post OR when the latest comment becomes invisible for front-office)
            //   - link, unlink an attachment, update an attached file on child comments that may be seen in front office (note: link order changes are not recorded because it doesn't affect whether users have seen latest content changes)
           
$this->contents_last_updated_ts = $db_row->post_contents_last_updated_ts;

           
$this->creator_user_ID = $db_row->post_creator_user_ID;   // Needed for history display
           
$this->lastedit_user_ID = $db_row->post_lastedit_user_ID; // Needed for history display
           
$this->assigned_user_ID = $db_row->post_assigned_user_ID;
           
$this->dateset = $db_row->post_dateset;
           
$this->status = $db_row->post_status;
           
$this->content = $db_row->post_content;
           
$this->titletag = $db_row->post_titletag;
           
$this->pst_ID = $db_row->post_pst_ID;
           
$this->datedeadline = $db_row->post_datedeadline;
           
$this->priority = $db_row->post_priority;
           
$this->locale = $db_row->post_locale;
           
$this->locale_visibility = $db_row->post_locale_visibility;
           
$this->wordcount = $db_row->post_wordcount;
           
$this->notifications_status = $db_row->post_notifications_status;
           
$this->notifications_ctsk_ID = $db_row->post_notifications_ctsk_ID;
           
$this->notifications_flags = $db_row->post_notifications_flags;
           
$this->comment_status = $db_row->post_comment_status;            // Comments status
           
$this->featured = $db_row->post_featured;
           
$this->parent_ID = $db_row->post_parent_ID === NULL ? NULL : intval( $db_row->post_parent_ID );
           
$this->igrp_ID = $db_row->post_igrp_ID;

           
// echo 'renderers=', $db_row->post_renderers;
           
$this->renderers = $db_row->post_renderers;

           
$this->excerpt = $db_row->post_excerpt;
           
$this->excerpt_autogenerated = $db_row->post_excerpt_autogenerated;

           
// Location

           
if ( ! empty ( $db_row->post_ctry_ID ) )
            {
               
$this->ctry_ID = $db_row->post_ctry_ID;
            }

            if ( ! empty (
$db_row->post_rgn_ID ) )
            {
               
$this->rgn_ID = $db_row->post_rgn_ID;
            }

            if ( ! empty (
$db_row->post_subrg_ID ) )
            {
               
$this->subrg_ID = $db_row->post_subrg_ID;
            }

            if ( ! empty (
$db_row->post_city_ID ) )
            {
               
$this->city_ID = $db_row->post_city_ID;
            }

           
// Voting fields:
           
$this->addvotes = $db_row->post_addvotes;
           
$this->countvotes = $db_row->post_countvotes;
        }

       
modules_call_method( 'constructor_item', array( 'Item' => & $this ) );
    }


   
/**
     * Compare two Items based on the title
     *
     * @param Item A
     * @param Item B
     * @return number -1 if A->title < B->title, 1 if A->title > B->title, 0 if A->title == B->title
     */
   
static function compare_items_by_title( $a_Item, $b_Item )
    {
        if(
$a_Item == NULL || $b_Item == NULL )
        {
           
debug_die('Invalid item objects received to compare.');
        }

        return
strcmp( $a_Item->title, $b_Item->title );
    }


   
/**
     * Compare two Items based on the short title if this field is not empty otherwise use title to compare
     *
     * @param Item A
     * @param Item B
     * @return number -1 if A->short_title < B->short_title, 1 if A->short_title > B->short_title, 0 if A->short_title == B->short_title
     */
   
static function compare_items_by_short_title( $a_Item, $b_Item )
    {
        if(
$a_Item == NULL || $b_Item == NULL )
        {
           
debug_die('Invalid item objects received to compare.');
        }

        if( ! empty(
$a_Item->short_title ) && $a_Item->get_type_setting( 'use_short_title' ) == 'optional' )
        {    
// Use short title only if it is not empty and allowed by item type:
           
$a_title_value =  $a_Item->short_title;
        }
        else
        {    
// Otherwise use title:
           
$a_title_value =  $a_Item->title;
        }

        if( ! empty(
$b_Item->short_title ) && $b_Item->get_type_setting( 'use_short_title' ) == 'optional' )
        {    
// Use short title only if it is not empty and allowed by item type:
           
$b_title_value =  $b_Item->short_title;
        }
        else
        {    
// Otherwise use title:
           
$b_title_value =  $b_Item->title;
        }

        return
strcmp( $a_title_value, $b_title_value );
    }


   
/**
     * Compare two Items based on the order field
     *
     * @param Item A
     * @param Item B
     * @return number -1 if A->order < B->order, 1 if A->order > B->order, 0 if A->order == B->order
     */
   
static function compare_items_by_order( $a_Item, $b_Item )
    {
        if(
$a_Item == NULL || $b_Item == NULL )
        {
           
debug_die('Invalid item objects received to compare.');
        }

       
// Get item orders depending on current category:
       
$a_item_order = isset( $a_Item->sort_current_cat_ID ) ? $a_Item->sort_current_cat_ID : NULL;
       
$b_item_order = isset( $b_Item->sort_current_cat_ID ) ? $b_Item->sort_current_cat_ID : NULL;

        if(
$a_Item->get_order( $a_item_order ) == NULL )
        {
            return
$b_Item->get_order( $b_item_order ) == NULL ? 0 : 1;
        }
        elseif(
$b_Item->get_order( $b_item_order ) == NULL )
        {
            return -
1;
        }

        return (
$a_Item->get_order( $a_item_order ) < $b_Item->get_order( $b_item_order ) ) ? -1 : ( ( $a_Item->get_order( $a_item_order ) > $b_Item->get_order( $b_item_order ) ) ? 1 : 0 );
    }


   
/**
     * Set creator user
     *
     * @param string login
     */
   
function set_creator_by_login( $login )
    {
       
$UserCache = & get_UserCache();
        if( (
$creator_User = &$UserCache->get_by_login( $login ) ) !== false )
        {
           
$this->set( $this->creator_field, $creator_User->ID );
        }
    }


   
/**
     * Assign user to the item by user ID or by user login
     *
     * @todo use extended dbchange instead of set_param...
     * @todo Normalize to set_assigned_User!?
     *
     * @param integer User ID
     * @param string User login
     * @param boolean TRUE to update DB
     * @return boolean TRUE on success, FALSE if user cannot be assigned on this item
     */
   
function assign_to( $user_ID, $user_login = '', $dbupdate = true /* BLOAT!? */ )
    {
        global
$Messages;

        if( ! empty(
$user_ID ) )
        {
// Get an user by ID to check perms
           
$UserCache = & get_UserCache();
           
$assigned_User = & $UserCache->get_by_ID( $user_ID, false, false );
        }
        else if( ! empty(
$user_login ) )
        {
// If an assigned user ID is empty find it by user login
           
$UserCache = & get_UserCache();
           
$assigned_User = & $UserCache->get_by_login( $user_login );
            if( empty(
$assigned_User ) )
            {
// Invalid user login was entered
               
$Messages->add( sprintf( T_('User %s doesn\'t exist!'), '<b>'.$user_login.'</b>' ), 'error' );
                return
false;
            }
        }

        if( ! empty(
$assigned_User ) )
        {
// Check if the selected user can be assigned to this item
           
$this->load_Blog();
            if(
$assigned_User->check_perm( 'blog_can_be_assignee', 'edit', false, $this->Blog->ID ) )
            {
// User exists and has permission to be assigned user to items of the blog
               
$user_ID = $assigned_User->ID;
            }
            else
            {
// No permission to be assigned
               
$Messages->add( sprintf( T_('User %s cannot be assigned to the items of this blog!'), '<b>'.$assigned_User->login.'</b>' ), 'error' );
                return
false;
            }
        }

        if( ! empty(
$user_ID ) )
        {
            if(
$dbupdate )
            {
// Record ID for DB:
               
$this->set_param( 'assigned_user_ID', 'number', $user_ID, true );
            }
            else
            {
               
$this->assigned_user_ID = $user_ID;
            }
           
$UserCache = & get_UserCache();
           
$this->assigned_User = & $UserCache->get_by_ID( $user_ID );
        }
        else
        {
           
// fp>> DO NOT set (to null) immediately OR it may KILL the current User object (big problem if it's the Current User)
           
unset( $this->assigned_User );
            if(
$dbupdate )
            {
// Record ID for DB:
               
$this->set_param( 'assigned_user_ID', 'number', NULL, true );
            }
            else
            {
               
$this->assigned_User = NULL;
            }
           
$this->assigned_user_ID = NULL;
        }

        return
true;
    }


   
/**
     * Template function: display author/creator of item
     *
     */
   
function author( $params = array() )
    {
       
// Make sure we are not missing any param:
       
$params = array_merge( array(
               
'before'         => ' ',
               
'after'          => ' ',
               
'link_text'      => 'preferredname', // auto | avatar_name | avatar_login | only_avatar | name | login | nickname | firstname | lastname | fullname | preferredname
               
'thumb_size'     => 'crop-top-32x32',
               
'thumb_class'    => '',
               
'thumb_zoomable' => false,
               
'login_mask'     => '', // example: 'text $login$ text'
               
'display_bubbletip' => true,
               
'nowrap'         => true,
            ),
$params );

       
// Load User
       
$this->get_creator_User();

       
$r = $this->creator_User->get_identity_link( $params );

        echo
$params['before'].$r.$params['after'];
    }


   
/**
     * Template function: display user who edited the item last time
     *
     */
   
function lastedit_user( $params = array() )
    {
       
// Make sure we are not missing any param:
       
$params = array_merge( array(
               
'profile_tab'    => 'user',
               
'before'         => ' ',
               
'after'          => ' ',
               
'format'         => 'htmlbody',
               
'link_to'        => 'userpage',
               
'link_text'      => 'preferredname', // avatar_name | avatar_login | only_avatar | name | login | nickname | firstname | lastname | fullname | preferredname
               
'link_rel'       => '',
               
'link_class'     => '',
               
'thumb_size'     => 'crop-top-32x32',
               
'thumb_class'    => '',
               
'thumb_zoomable' => false,
            ),
$params );

       
// Load User
       
$this->get_lastedit_User();

        if(
$this->lastedit_User )
        {    
// Get a link to user profile page
           
$r = $this->lastedit_User->get_identity_link( $params );
        }
        else
        {    
// User was deleted
           
$r = T_('(deleted user)');
        }

        echo
$params['before'].$r.$params['after'];
    }


   
/**
     * Load data from Request form fields.
     *
     * This requires the blog (e.g. {@link $blog_ID} or {@link $main_cat_ID} to be set).
     *
     * @param boolean true if we are returning to edit mode (new, switchtab...)
     * @return boolean true if loaded data seems valid.
     */
   
function load_from_Request( $editing = false, $creating = false )
    {
        global
$default_locale, $localtimenow, $Blog, $Plugins;
        global
$item_typ_ID;

       
// LOCALE:
       
if( param( 'post_locale', 'string', NULL ) !== NULL )
        {
           
$this->set_from_Request( 'locale' );
        }

       
// LOCALE VISIBILITY:
       
if( param( 'post_locale_visibility', 'string', NULL ) !== NULL )
        {
           
$this->set_from_Request( 'locale_visibility' );
        }

        if(
param( 'source_version_item_ID', 'integer', NULL ) !== NULL )
        {    
// Temp flag to know this is a new version of this Item:
           
$this->source_version_item_ID = get_param( 'source_version_item_ID' );
        }

       
// POST TYPE:
       
$item_typ_ID = get_param( 'item_typ_ID' );
        if( empty(
$item_typ_ID ) )
        {
// Try to get this from request if it has been not initialized by controller:
           
$item_typ_ID = param( 'item_typ_ID', 'integer', NULL );
        }
        if( ! empty(
$item_typ_ID ) )
        {
// Set new post type ID only if it is defined on request:
           
$this->set( 'ityp_ID', $item_typ_ID );
        }

       
// Check if this Item type usage is not content block in order to hide several fields below:
       
$is_not_content_block = ( $this->get_type_setting( 'usage' ) != 'content-block' );

       
// URL associated with Item:
       
$post_url = param( 'post_url', 'string', NULL );
        if(
$post_url !== NULL )
        {
           
param_check_url( 'post_url', 'http-https' );
           
$this->set_from_Request( 'url' );
        }
        if( empty(
$post_url ) && $this->get_type_setting( 'use_url' ) == 'required' )
        {
// URL must be entered
           
param_check_not_empty( 'post_url', T_('Please provide a "Link To" URL.'), '' );
        }
        if(
is_pro() )
        {    
// Only PRO feature for using of post link URL as an External Canonical URL:
           
$this->set_setting( 'external_canonical_url', param( 'post_external_canonical_url', 'integer', 0 ) );
        }

       
// Item parent ID:
       
$post_parent_ID = param( 'post_parent_ID', 'integer', NULL );
        if(
$post_parent_ID !== NULL )
        {    
// If item parent ID is entered:
           
$ItemCache = & get_ItemCache();
            if(
$ItemCache->get_by_ID( $post_parent_ID, false, false ) )
            {    
// Save only ID of existing item:
               
$this->set_from_Request( 'parent_ID' );
            }
            else
            {    
// Display an error of the entered item parent ID is incorrect:
               
param_error( 'post_parent_ID', T_('The parent ID is not a correct Item ID.') );
            }
        }
        if( empty(
$post_parent_ID ) )
        {    
// If empty parent ID is entered:
           
if( $this->get_type_setting( 'use_parent' ) == 'required' )
            {    
// Item parent ID must be entered:
               
param_check_not_empty( 'post_parent_ID', T_('Please provide a parent ID.'), '' );
            }
            else
            {    
// Remove parent ID:
               
$this->set_from_Request( 'parent_ID' );
            }
        }

       
// Single/page view:
       
if( check_user_perm( 'blog_edit_ts', 'edit', false, $Blog->ID ) &&
            (
$single_view = param( 'post_single_view', 'string', NULL ) ) !== NULL )
        {    
// If user has a permission to edit advanced properties of items:
           
if( $this->get( 'status' ) == 'redirected' )
            {    
// Single view of "Redirected" item can be only redirected as well:
               
$single_view = 'redirected';
            }
            elseif(
$this->previous_status == 'redirected' && $single_view == 'redirected' )
            {    
// Set single view to normal mode when item status is updating from redirected to another status:
               
$single_view = 'normal';
            }
           
$this->set( 'single_view', $single_view );
        }

        if( (
$this->status == 'redirected' || $this->get( 'single_view' ) == 'redirected' ) && empty( $this->url ) )
        {
// Note: post_url is not part of the simple form, so this message can be a little bit awkward there
           
param_error( 'post_url',
               
T_('If you want to redirect this post, you must specify an URL!').' ('.T_('Advanced properties panel').')',
               
T_('If you want to redirect this post, you must specify an URL!') );
        }

       
// ISSUE DATE / TIMESTAMP:
       
$this->load_Blog();
        if(
check_user_perm( 'admin', 'restricted' ) &&
           
check_user_perm( 'blog_edit_ts', 'edit', false, $this->Blog->ID ) )
        {
// Allow to update timestamp fields only if user has a permission to edit such fields
          //    and also if user has an access to back-office
           
$item_dateset = param( 'item_dateset', 'integer', NULL );
            if(
$item_dateset !== NULL )
            {
               
$this->set( 'dateset', $item_dateset );

                if(
$editing || $this->dateset == 1 )
                {
// We can use user date:
                   
if( param_date( 'item_issue_date', sprintf( T_('Please enter a valid issue date using the following format: %s'), '<code>'.locale_input_datefmt().'</code>' ), true )
                        &&
param_time( 'item_issue_time' ) )
                    {
// only set it, if a (valid) date and time was given:
                       
$this->set( 'issue_date', form_date( get_param( 'item_issue_date' ), get_param( 'item_issue_time' ) ) ); // TODO: cleanup...
                   
}
                }
                elseif(
$this->dateset == 0 )
                {
// Set date to NOW:
                   
$this->set( 'issue_date', date( 'Y-m-d H:i:s', $localtimenow ) );
                }
            }
        }

       
// SLUG:
       
if( param( 'post_urltitle', 'string', NULL ) !== NULL )
        {
           
// Replace special chars/umlauts:
           
load_funcs( 'locales/_charset.funcs.php' );
           
// Split slug url titles with comma because the function replace_special_chars()
            // converts `,` to `-`, so separate slugs are concatenated in single, that's wrong:
           
$post_urltitle = explode( ',', get_param( 'post_urltitle' ) );
            foreach(
$post_urltitle as $u => $slug_urltitle )
            {
               
$post_urltitle[ $u ] = replace_special_chars( $slug_urltitle, $this->get( 'locale' ) );
                if( empty(
$post_urltitle[ $u ] ) )
                {    
// Unset empty slug in order to create auto slug:
                   
unset( $post_urltitle[ $u ] );
                    continue;
                }
               
// Added in May 2017; but old slugs are not converted yet.
               
if( preg_match( '#^[^a-z0-9]*[0-9]*[^a-z0-9]*$#i', $post_urltitle[ $u ] ) )
                {    
// Display error if one of item slugs doesn't contain at least 1 non-numeric character:
                   
param_error( 'post_urltitle', T_('All slugs must contain at least 1 non-numeric character.') );
                }
            }
           
// Append old slugs at the end because they are not deleted on updating of the Item,
            // and update array of the cached slugs from DB in order to display proper slugs after submit the forms with errors:
           
$this->get_slugs();
           
$this->slugs = array_unique( array_merge( $post_urltitle, $this->slugs ) );
           
// Set new post urltitle:
           
$this->set( 'urltitle', implode( ', ', $post_urltitle ) );
        }

        if(
$is_not_content_block )
        {    
// Save title tag, meta description and meta keywords for item with type usage except of content block:
            // <title> TAG:
           
$titletag = param( 'titletag', 'string', NULL );
            if(
$titletag !== NULL )
            {
               
$this->set_from_Request( 'titletag', 'titletag' );
            }
            if( empty(
$titletag ) && $this->get_type_setting( 'use_title_tag' ) == 'required' )
            {
// Title tag must be entered
               
param_check_not_empty( 'titletag', T_('Please provide a title tag.'), '' );
            }

           
// <meta> DESC:
           
$metadesc = param( 'metadesc', 'string', NULL );
            if(
$metadesc !== NULL ) {
               
$this->set_setting( 'metadesc', get_param( 'metadesc' ) );
            }
            if( empty(
$metadesc ) && $this->get_type_setting( 'use_meta_desc' ) == 'required' )
            {
// Meta description must be entered
               
param_check_not_empty( 'metadesc', T_('Please provide a meta description.'), '' );
            }

           
// <meta> KEYWORDS:
           
$metakeywords = param( 'metakeywords', 'string', NULL );
            if(
$metakeywords !== NULL ) {
               
$this->set_setting( 'metakeywords', get_param( 'metakeywords' ) );
            }
            if( empty(
$metakeywords ) && $this->get_type_setting( 'use_meta_keywds' ) == 'required' )
            {
// Meta keywords must be entered
               
param_check_not_empty( 'metakeywords', T_('Please provide the meta keywords.'), '' );
            }
        }

       
// TAGS:
       
$item_tags = param( 'item_tags', 'string', NULL );
        if(
$item_tags !== NULL )
        {
           
$this->set_tags_from_string( get_param('item_tags') );
           
// Update setting 'suggest_item_tags' of the current User
           
global $UserSettings;
           
$UserSettings->set( 'suggest_item_tags', param( 'suggest_item_tags', 'integer', 0 ) );
           
$UserSettings->dbupdate();
        }
        if( empty(
$item_tags ) && $this->get_type_setting( 'use_tags' ) == 'required' )
        {
// Tags must be entered
           
param_check_not_empty( 'item_tags', T_('Please provide at least one tag.'), '' );
        }

       
// WORKFLOW stuff:
       
$this->load_workflow_from_Request();

       
// FEATURED checkbox:
       
if( check_user_perm( 'blog_edit_ts', 'edit', false, $Blog->ID ) )
        {    
// If user has a permission to edit advanced properties of items:
           
$this->set( 'featured', param( 'item_featured', 'integer', 0 ), false );
        }

       
// MUST READ checkbox:
       
if( is_pro() &&
            (
$item_Blog = & $this->get_Blog() ) &&
           
$item_Blog->get_setting( 'track_unread_content' ) )
        {    
// Update only for PRO version and when tracking of unread content is enabled for collection:
           
$this->set_setting( 'mustread', param( 'item_mustread', 'integer', 0 ) );
        }

        if(
$is_not_content_block )
        {    
// Save "hide teaser" and goal for item with type usage except of content block:
            // HIDE TEASER checkbox:
           
$this->set_setting( 'hide_teaser', param( 'item_hideteaser', 'integer', 0 ) );

           
// User Tagging:
           
if( param( 'user_tags', 'string', NULL ) !== NULL )
            {
               
$this->set_setting( 'user_tags', trim( get_param( 'user_tags' ), ' ,' ) );
            }

           
// Goal ID:
           
if( check_user_perm( 'blog_edit_ts', 'edit', false, $Blog->ID ) )
            {    
// If user has a permission to edit advanced properties of items:
               
$goal_ID = param( 'goal_ID', 'integer', NULL );
                if(
$goal_ID !== NULL )
                {    
// Save only if it is provided:
                   
$this->set_setting( 'goal_ID', $goal_ID, true );
                }
            }
        }

        if(
$this->get_type_setting( 'allow_switchable' ) )
        {    
// Includes switchable content:
           
$this->set_setting( 'switchable', param( 'item_switchable', 'integer', 0 ) );
           
$this->set_setting( 'switchable_params', param( 'item_switchable_params', 'string' ) );
        }

       
// OWNER:
       
$this->creator_user_login = param( 'item_owner_login', 'string', NULL );
        if(
check_user_perm( 'users', 'edit' ) && param( 'item_owner_login_displayed', 'string', NULL ) !== NULL )
        {    
// only admins can change the owner..
           
if( param_check_not_empty( 'item_owner_login', T_('Please enter valid owner login.') ) )
            {    
// If valid user login is entered:
               
if( param( 'item_create_user', 'integer', 0 ) )
                {    
// Try to create new user if it is checked on the edit item form:
                   
$UserCache = & get_UserCache();

                   
// Convert new entered login to proper login format:
                   
$this->creator_user_login = preg_replace( '/[^a-z0-9_\-\. ]/i', '', $this->creator_user_login );
                   
$this->creator_user_login = str_replace( ' ', '_', $this->creator_user_login );
                   
$this->creator_user_login = utf8_substr( $this->creator_user_login, 0, 20 );
                   
set_param( 'item_owner_login', $this->creator_user_login );

                    if( (
$creator_User = & $UserCache->get_by_login( $this->creator_user_login ) ) !== false )
                    {    
// Display error if user already exists:
                       
param_error( 'item_owner_login', sprintf( T_('User "%s" already exists.'), $this->creator_user_login ) );
                    }
                    else
                    {    
// Create new user:
                       
$item_new_User = new User();
                       
$item_new_User->set( 'login', $this->creator_user_login );
                       
$item_new_User->set( 'email', $this->creator_user_login.'@dummy.null' );
                       
$item_new_User->set( 'source', 'created alongside post' );
                       
$item_new_User->set( 'pass', '' );
                       
$item_new_User->set( 'salt', '' );
                       
$item_new_User->set( 'pass_driver', 'nopass' );
                       
$item_new_User->dbinsert();
                       
// Update user login cache with new created User:
                       
$UserCache->cache_login[ $this->creator_user_login ] = $item_new_User;
                       
// Uncheck the checkbox to don't suggest create new user on next form updating because the user already has been created with requested login:
                       
set_param( 'item_create_user', 0 );
                    }
                }

                if(
param_check_login( 'item_owner_login', true ) )
                {    
// Update item's owner if the user is detected in DB by the entered login:
                   
$this->set_creator_by_login( $this->creator_user_login );
                }
            }
        }

       
// LOCATION COORDINATES:
       
if( $this->get_type_setting( 'use_coordinates' ) != 'never' )
        {
// location coordinates are enabled, save map settings
           
param( 'item_latitude', 'double', NULL ); // get par value
           
$this->set_setting( 'latitude', get_param( 'item_latitude' ), true );
           
param( 'item_longitude', 'double', NULL ); // get par value
           
$this->set_setting( 'longitude', get_param( 'item_longitude' ), true );
           
param( 'google_map_zoom', 'integer', NULL ); // get par value
           
$this->set_setting( 'map_zoom', get_param( 'google_map_zoom' ), true );
           
param( 'google_map_type', 'string', NULL ); // get par value
           
$this->set_setting( 'map_type', get_param( 'google_map_type' ), true );
            if(
$this->get_type_setting( 'use_coordinates' ) == 'required' )
            {
// The location coordinates are required
               
param_check_not_empty( 'item_latitude', T_('Please provide a latitude.'), '' );
               
param_check_not_empty( 'item_longitude', T_('Please provide a longitude.'), '' );
            }
        }

       
// CUSTOM FIELDS:
       
$this->load_custom_fields_from_Request();

       
// COMMENTS:
       
if( $this->allow_comment_statuses() )
        {
// Save status of "Allow comments for this item" (only if comments are allowed in this blog, and by current post type
           
$post_comment_status = param( 'post_comment_status', 'string', 'open' );
            if( !empty(
$post_comment_status ) )
            {
// 'open' or 'closed' or ...
               
$this->set_from_Request( 'comment_status' );
            }
        }

       
// MESSAGE BEFORE COMMENT FORM:
       
if( $this->get_type_setting( 'allow_comment_form_msg' ) )
        {    
// Save a mesage before comment form only if it is allowed by item type:
           
$comment_form_msg = param( 'comment_form_msg', 'text', NULL );
           
$this->set_setting( 'comment_form_msg', $comment_form_msg, true );
        }

       
// EXPIRY DELAY:
       
if( check_user_perm( 'blog_edit_ts', 'edit', false, $Blog->ID ) )
        {    
// If user has a permission to edit advanced properties of items:
           
$expiry_delay = param_duration( 'expiry_delay' );
            if( empty(
$expiry_delay ) )
            {
// Check if we have 'expiry_delay' param set as string from simple or mass form
               
$expiry_delay = param( 'expiry_delay', 'string', NULL );
            }
            if( empty(
$expiry_delay ) && $this->get_type_setting( 'use_comment_expiration' ) == 'required' )
            {
// Comment expiration must be entered
               
param_check_not_empty( 'expiry_delay', T_('Please provide a comment expiration delay.'), '' );
            }
           
$this->set_setting( 'comment_expiry_delay', $expiry_delay, true );
        }

       
// EXTRA PARAMS FROM MODULES:
       
modules_call_method( 'update_item_settings', array( 'edited_Item' => $this ) );

       
// RENDERERS:
       
$item_Blog = & $this->get_Blog();
        if(
is_admin_page() || $item_Blog->get_setting( 'in_skin_editing_renderers' ) )
        {    
// If text renderers are allowed to update from front-office:
           
if( param( 'renderers_displayed', 'integer', 0 ) )
            {    
// Use "renderers" value only if it has been displayed (may be empty):
               
$renderers = $Plugins->validate_renderer_list( param( 'renderers', 'array:string', array() ), array( 'Item' => & $this ) );
               
$this->set( 'renderers', $renderers );
            }
            else
            {
               
$renderers = $this->get_renderers_validated();
            }
        }
        else
        {    
// Don't allow to update the text renderers:
           
$renderers = $this->get_renderers();
        }

        if(
$this->get_type_setting( 'use_short_title' ) == 'optional' )
        {    
// Short title:
           
$post_short_title = param( 'post_short_title', 'htmlspecialchars', NULL );
           
$this->set_from_Request( 'short_title', 'post_short_title', true );
        }

       
// CONTENT + TITLE:
       
if( $this->get_type_setting( 'allow_html' ) )
        {    
// HTML is allowed for this post, we'll accept HTML tags:
           
$text_format = 'html';
        }
        else
        {    
// HTML is disallowed for this post, we'll encode all special chars:
           
$text_format = 'htmlspecialchars';
        }

       
$editor_code = param( 'editor_code', 'string', NULL );
        if(
$editor_code )
        {
// Update item editor code if it was explicitly set
           
$this->set_setting( 'editor_code', $editor_code );
        }

       
// Never allow html content on post titles:  (fp> probably so as to not mess up backoffice and all sorts of tools)
       
param( 'post_title', 'htmlspecialchars', NULL );
       
// Title checking:
       
if( ( ! $editing || $creating ) && $this->get_type_setting( 'use_title' ) == 'required' ) // creating is important, when the action is create_edit
       
{
           
param_check_not_empty( 'post_title', T_('Please provide a title.'), '' );
        }

       
$content = param( 'content', $text_format, NULL );
        if(
$content !== NULL )
        {
           
// Do some optional filtering on the content
            // Typically stuff that will help the content to validate
            // Useful for code display.
            // Will probably be used for validation also.
            // + APPLY RENDERING from Rendering Plugins:
           
$Plugins_admin = & get_Plugins_admin();
           
$params = array(
                   
'object_type' => 'Item',
                   
'object'      => & $this,
                   
'object_Blog' => & $this->Blog
               
);
           
$Plugins_admin->filter_contents( $GLOBALS['post_title'] /* by ref */, $GLOBALS['content'] /* by ref */, $renderers, $params /* by ref */ );

           
// Format raw HTML input to cleaned up and validated HTML:
           
param_check_html( 'content', T_('Invalid content.') );
           
$content = prepare_item_content( get_param( 'content' ) );

           
$this->set( 'content', $content );
        }
        if( empty(
$content ) && $this->get_type_setting( 'use_text' ) == 'required' )
        {
// Content must be entered
           
param_check_not_empty( 'content', T_('Please enter some text.'), '' );
        }

       
// Set title only here because it may be filtered by plugins above:
       
$this->set( 'title', get_param( 'post_title' ), true );

        if(
$is_not_content_block )
        {    
// Save excerpt for item with type usage except of content block:
            // EXCERPT: (must come after content (in order to handle excerpt_autogenerated))
           
$post_excerpt = param( 'post_excerpt', 'text', NULL );
            if(
$post_excerpt !== NULL )
            {    
// The form has sent an excerpt field:
               
$post_excerpt_autogenerated = param( 'post_excerpt_autogenerated', 'integer', 0 );
               
$this->set_from_Request( 'excerpt_autogenerated' );
                if( !
$this->get( 'excerpt_autogenerated' ) )
                {    
// The post excerpt must be no longer auto-generated:
                    // NOTE: if the new excerpt is empty, set() will switch back to autogeneration:
                   
$this->set_from_Request( 'excerpt' );
                }
            }

            if( empty(
$post_excerpt ) && $this->get_type_setting( 'use_excerpt' ) == 'required' )
            {
// Content must be entered (this should happen even if no excerpt field was submitted)
               
param_check_not_empty( 'post_excerpt', T_('Please provide an excerpt.'), '' );
            }
        }

       
// LOCATION (COUNTRY -> CITY):
       
load_funcs( 'regional/model/_regional.funcs.php' );
        if(
$this->country_visible() )
        {
// Save country
           
$country_ID = param( 'item_ctry_ID', 'integer', 0 );
           
$country_is_required = $this->get_type_setting( 'use_country' ) == 'required'
                   
&& countries_exist();
           
param_check_number( 'item_ctry_ID', T_('Please select a country'), $country_is_required );
           
$this->set_from_Request( 'ctry_ID', 'item_ctry_ID', true );
        }

        if(
$this->region_visible() )
        {
// Save region
           
$region_ID = param( 'item_rgn_ID', 'integer', 0 );
           
$region_is_required = $this->get_type_setting( 'use_region' ) == 'required'
                   
&& regions_exist( $country_ID );
           
param_check_number( 'item_rgn_ID', T_('Please select a region'), $region_is_required );
           
$this->set_from_Request( 'rgn_ID', 'item_rgn_ID', true );
        }

        if(
$this->subregion_visible() )
        {
// Save subregion
           
$subregion_ID = param( 'item_subrg_ID', 'integer', 0 );
           
$subregion_is_required = $this->get_type_setting( 'use_sub_region' ) == 'required'
                   
&& subregions_exist( $region_ID );
           
param_check_number( 'item_subrg_ID', T_('Please select a sub-region'), $subregion_is_required );
           
$this->set_from_Request( 'subrg_ID', 'item_subrg_ID', true );
        }

        if(
$this->city_visible() )
        {
// Save city
           
param( 'item_city_ID', 'integer', 0 );
           
$city_is_required = $this->get_type_setting( 'use_city' ) == 'required'
                   
&& cities_exist( $country_ID, $region_ID, $subregion_ID );
           
param_check_number( 'item_city_ID', T_('Please select a city'), $city_is_required );
           
$this->set_from_Request( 'city_ID', 'item_city_ID', true );
        }

        if(
is_admin_page() || $Blog->get_setting( 'in_skin_editing_category_order' ) )
        {    
// Item orders per category:
           
$post_cat_orders = param( 'post_cat_orders', 'array:string' );
           
$this->orders = array();
            foreach(
$post_cat_orders as $post_cat_ID => $post_cat_order )
            {
                if( isset(
$this->extra_cat_IDs ) &&
                       
is_array( $this->extra_cat_IDs ) &&
                       
in_array( $post_cat_ID, $this->extra_cat_IDs ) )
                {    
// Set order only for selected category:
                   
$this->orders[ $post_cat_ID ] = ( $post_cat_order === '' ? NULL : floatval( $post_cat_order ) );
                }
            }
        }

       
// Call plugins events to load additional Item fields:
       
$Plugins->trigger_event( 'ItemLoadFromRequest', $params = array( 'Item' => & $this ) );

        return !
param_errors_detected();
    }

   
/**
     * Load workflow properties from Request form fields.
     *
     * @return boolean TRUE if loaded data seems valid, FALSE if some errors or workflow is not allowed or no any property has been changed
     */
   
function load_workflow_from_Request()
    {
       
// Get Item's Collection for settings and permissions validation:
       
$item_Blog = & $this->get_Blog();

        if( (
$this->get_type_setting( 'usage' ) != 'content-block' ) && // Item types "Content Block" cannot have the workflow properties
           
$this->can_edit_workflow() ) // Current User has a permission to edit at least one workflow property
       
{    // Update workflow properties only when all conditions above is true:
           
if( $this->can_edit_workflow( 'status' ) &&
               
param( 'item_st_ID', 'integer', NULL ) !== NULL )
            {    
// Task status:
               
$ItemTypeCache = & get_ItemTypeCache();
               
$current_ItemType = $ItemTypeCache->get_by_ID( $this->get( 'ityp_ID' ) );
                if(
get_param( 'item_st_ID' ) === 0 )
                {    
// Store NULL value instead of 0 in DB:
                   
set_param( 'item_st_ID', NULL );
                }
                if(
in_array( get_param( 'item_st_ID' ), $current_ItemType->get_applicable_post_status() ) || get_param( 'item_st_ID' ) === NULL )
                {    
// Save only task status which is allowed for item's type:
                   
$this->set_from_Request( 'pst_ID', 'item_st_ID', true );
                }
                else
                {    
// If the submitted task status is not allowed for item's type:
                   
param_error( 'item_st_ID', sprintf( T_('Invalid task status for post type %s'), $current_ItemType->get_name() ) );
                }
            }

            if(
$this->can_edit_workflow( 'user' ) &&
               
param( 'item_assigned_user_ID', 'integer', NULL ) !== NULL )
            {    
// Assigned to:
               
$item_assigned_user_ID = get_param( 'item_assigned_user_ID' );
               
$item_assigned_user_login = param( 'item_assigned_user_login', 'string', NULL );
               
$this->assign_to( $item_assigned_user_ID, $item_assigned_user_login );
            }

            if(
$this->can_edit_workflow( 'priority' ) &&
               
param( 'item_priority', 'integer', NULL ) !== NULL )
            {    
// Priority:
               
if( get_param( 'item_priority' ) === 0 )
                {    
// Store NULL value instead of 0 in DB:
                   
set_param( 'item_priority', NULL );
                }
               
$this->set_from_Request( 'priority', 'item_priority', true );
            }

            if(
$this->can_edit_workflow( 'deadline' ) &&
               
param_date( 'item_deadline', T_('Please enter a valid deadline.'), false, NULL ) !== NULL )
            {    
// Deadline:
               
param_time( 'item_deadline_time', '', false, false, true, true );
               
$item_deadline_time = get_param( 'item_deadline' ) != '' ? substr( get_param( 'item_deadline_time' ), 0, 5 ) : '';
               
$item_deadline_datetime = trim( form_date( get_param( 'item_deadline' ), $item_deadline_time ) );
                if( ! empty(
$item_deadline_datetime ) )
                {    
// Append seconds because they are not entered on the form but they are stored in the DB field "post_datedeadline" as date format "YYYY-mm-dd HH:ii:ss":
                   
$item_deadline_datetime .= ':00';
                }
               
$this->set( 'datedeadline', $item_deadline_datetime, true );
            }

           
// Return TRUE when no errors and at least one workflow property has been changed:
           
return ! param_errors_detected() && (
                isset(
$this->dbchanges['post_assigned_user_ID'] ) ||
                isset(
$this->dbchanges['post_priority'] ) ||
                isset(
$this->dbchanges['post_pst_ID'] ) ||
                isset(
$this->dbchanges['post_datedeadline'] ) );
        }

       
// If workflow properties are not allowed to be stored for this Item by current User:
       
return false;
    }

   
/**
     * Load custom fields values from Request form fields
     *
     * @param boolean TRUE to load only custom fields which are allowed to be updated with internal comment
     * @return boolean TRUE if loaded data seems valid, FALSE if some errors or no any property has been changed
     */
   
function load_custom_fields_from_Request( $meta = NULL )
    {
       
$custom_fields = $this->get_type_custom_fields();
       
$this->dbchanges_custom_fields = array();
       
$custom_fields_changed = false;
        foreach(
$custom_fields as $custom_field )
        {
// update each custom field
           
if( $meta === true && ! $custom_field['meta'] )
            {    
// Skip not meta custom field when it is requested:
               
continue;
            }
           
$param_name = 'item_cf_'.$custom_field['name'];
           
$param_error = false;
            if(
isset_param( $param_name ) )
            {
// param is set
               
switch( $custom_field['type'] )
                {
                    case
'double':
                       
$param_type = 'double';
                       
$field_value = param( $param_name, 'string', NULL );
                        if( ! empty(
$field_value ) && ! preg_match( '/^(\+|-)?[0-9 \'.,]+([.,][0-9]+)?$/', $field_value ) ) // we could have used is_numeric here but this is how "double" type is checked in the param.funcs.php
                       
{
                           
param_error( $param_name, sprintf( T_('Custom "%s" field must be a number'), $custom_field['label'] ) );
                           
$param_error = true;
                        }
                        break;
                    case
'html':
                    case
'text': // Keep html tags for text fields, they will be escaped at display
                       
$param_type = 'html';
                        break;
                    case
'url':
                       
$param_type = 'url';
                       
$field_value = param( $param_name, 'string', NULL );
                       
$url_error = validate_url( $field_value, 'http-https' );
                        if(
$url_error !== false )
                        {
                           
param_error( $param_name, $url_error );
                           
$param_error = true;
                        }
                        break;
                    case
'image':
                       
$param_type = 'integer';
                       
$field_value = param( $param_name, 'string', NULL );
                        if( ! empty(
$field_value ) && ! is_number( $field_value ) )
                        {
                           
param_error( $param_name, sprintf( T_('Custom "%s" field must be a number'), $custom_field['label'] ) );
                           
$param_error = true;
                        }
                        break;
                    case
'varchar':
                    default:
                       
$param_type = 'string';
                        break;
                }
                if( !
$param_error )
                {
                   
param( $param_name, $param_type, NULL ); // get par value
               
}
                if(
$custom_field['required'] && ( $custom_field['public'] || is_admin_page() ) )
                {    
// Check required field only when it is public:
                   
param_check_not_empty( $param_name, sprintf( T_('Custom "%s" cannot be empty.'), $custom_field['label'] ) );
                }
               
$custom_field_make_null = $custom_field['type'] != 'double'; // store '0' values in DB for numeric fields

               
$custom_field_value = $this->get_custom_field_value( $custom_field['name'] );
                if(
$custom_field_value !== NULL && $custom_field_value !== false )
                {    
// Store previous value in order to save this in archived version:
                   
$this->dbchanges_custom_fields[ $custom_field['name'] ] = $custom_field_value;
                   
// Flag to know custom fields were changed:
                   
$this->dbchanges_flags['custom_fields'] = true;
                }
               
$this->set_custom_field( $custom_field['name'], get_param( $param_name ), 'value', $custom_field_make_null );
                if( !
$custom_fields_changed && $custom_field_value != get_param( $param_name ) )
                {    
// Mark that at least one custom field was changed:
                   
$custom_fields_changed = true;
                }
            }
        }
        foreach(
$custom_fields as $custom_field )
        {    
// Update computed custom fields after when all fields we updated above:
           
if( $custom_field['type'] == 'computed' )
            {    
// Set a value by special function because we don't submit value for such fields and compute a value by formula automatically:
               
$this->set_custom_field( $custom_field['name'], $this->get_custom_field_computed( $custom_field['name'] ) );
            }
        }

       
// Return TRUE when no errors and ata least one custom field has been changed:
       
return ! param_errors_detected() && $custom_fields_changed;
    }


   
/**
     * Link attachments from temporary object to new created Item
     */
   
function link_from_Request()
    {
        global
$DB;

        if(
$this->ID == 0 )
        {    
// The item must be stored in DB:
           
return;
        }

       
$temp_link_owner_ID = param( 'temp_link_owner_ID', 'integer', 0 );

       
$TemporaryIDCache = & get_TemporaryIDCache();
        if( ! (
$TemporaryID = & $TemporaryIDCache->get_by_ID( $temp_link_owner_ID, false, false ) ) )
        {    
// No temporary object of attachments:
           
return;
        }

        if(
$TemporaryID->type != 'item' )
        {    
// Wrong temporary object:
           
return;
        }

       
// Load all links:
       
$LinkOwner = new LinkItem( new Item(), $TemporaryID->ID );
       
$LinkOwner->load_Links();

        if( empty(
$LinkOwner->Links ) )
        {    
// No links:
           
return;
        }

       
// Change link owner from temporary to message object:
       
$DB->query( 'UPDATE T_links
              SET link_itm_ID = '
.$this->ID.',
                  link_tmp_ID = NULL
            WHERE link_tmp_ID = '
.$TemporaryID->ID );

       
$item_slug_folder_name = 'quick-uploads/'.$this->get( 'urltitle' ).'/';

       
// Move all temporary files to folder of new created message:
       
foreach( $LinkOwner->Links as $item_Link )
        {
            if(
$item_File = & $item_Link->get_File() &&
               
$item_FileRoot = & $item_File->get_FileRoot() &&
               
$item_Link->is_single_linked_file() )
            {    
// If file is not linked to any other object:
               
if( ! file_exists( $item_FileRoot->ads_path.$item_slug_folder_name ) )
                {    
// Create if folder doesn't exist for files of new created message:
                   
mkdir_r( $item_FileRoot->ads_path.$item_slug_folder_name );
                }
               
$item_File->move_to( $item_FileRoot->type, $item_FileRoot->in_type_ID, $item_slug_folder_name.$item_File->get_name(), true );
            }
        }

        if(
$item_FileRoot && file_exists( $item_FileRoot->ads_path.'quick-uploads/tmp'.$TemporaryID->ID.'/' ) )
        {    
// Remove temp folder from disk completely:
           
rmdir_r( $item_FileRoot->ads_path.'quick-uploads/tmp'.$TemporaryID->ID.'/' );
        }

       
// Delete temporary object from DB:
       
$TemporaryID->dbdelete();
    }


   
/**
     * Template function: display anchor for permalinks to refer to.
     */
   
function anchor()
    {
        global
$Settings;

        echo
'<a id="'.$this->get_anchor_id().'"></a>';
    }


   
/**
     * @return string
     */
   
function get_anchor_id()
    {
       
// In case you have old cafelog permalinks, uncomment the following line:
        // return preg_replace( '/[^a-zA-Z0-9_\.-]/', '_', $this->title );

       
return 'item_'.$this->ID;
    }


   
/**
     * Template tag
     */
   
function anchor_id()
    {
        echo
$this->get_anchor_id();
    }


   
/**
     * Template function: display assignee of item
     *
     * @param string
     * @param string
     * @param string Output format, see {@link format_to_output()}
     */
   
function assigned_to( $before = '', $after = '', $format = 'htmlbody' )
    {
        if(
$this->get_assigned_User() )
        {
            echo
$before;
            echo
$this->assigned_User->get_identity_link( array(
                   
'format'    => $format,
                   
'link_text' => 'name',
                ) );
            echo
$after;
        }
    }


   
/**
     * Template function: display assignee of item with configurable params
     *
     * @params array
     */
   
function assigned_to2( $params = array() )
    {
        if(
$this->get_assigned_User() )
        {
           
$params = array_merge( array(
                 
'before' => '',
                   
'after' => '',
                   
'format' => 'htmlbody',
                   
'link_text' => 'only_avatar',
                ),
$params );
            echo
$params['before'].$this->assigned_User->get_identity_link( $params ).$params['after'];
        }
    }


   
/**
     * Get list of assigned user options
     *
     * @uses UserCache::get_blog_member_option_list()
     * @return string HTML select options list
     */
   
function get_assigned_user_options()
    {
       
$UserCache = & get_UserCache();
       
$UserCache->clear();
        return
$UserCache->get_blog_member_option_list( $this->get_blog_ID(), $this->assigned_user_ID,
                           
true,    ($this->ID != 0) /* if this Item is already serialized we'll load the default anyway */ );
    }


   
/**
     * Check if user can see comments on this post, which he cannot if they
     * are disabled for the Item or never allowed for the blog.
     *
     * @param boolean true will display why user can't see comments
     * @return boolean
     */
   
function can_see_comments( $display = false )
    {
        global
$Settings, $disp;

        if(
$disp == 'terms' )
        {    
// Don't display the comments on page with terms & conditions:
           
return false;
        }

        if( !
$this->get_type_setting( 'use_comments' ) )
        {
// Comments are not allowed on this post by post type
           
return false;
        }

        if(
$this->get_type_setting( 'allow_disabling_comments' ) && ( $this->comment_status == 'disabled' ) )
        {
// Comments are disabled on this post
           
return false;
        }

        if(
$this->check_blog_settings( 'allow_view_comments' ) )
        {
// User is allowed to see comments
           
return true;
        }

        if( !
$display )
        {
            return
false;
        }

       
$this->load_Blog();
       
$number_of_comments = $this->get_number_of_comments( 'published' );
       
$allow_view_comments = $this->Blog->get_setting( 'allow_view_comments' );
       
$user_can_be_validated = check_user_status( 'can_be_validated' );

        if( (
$allow_view_comments != 'any' ) && ( $user_can_be_validated ) )
        {
// change allow view comments to activated, because user is logged in but the account is not activated, and anomnymous users can't see comments
           
$allow_view_comments = 'active_users';
        }

       
// Set display text
       
switch( $allow_view_comments )
        {
            case
'active_users':
               
// users must activate their accounts before they can see the comments
               
if( $number_of_comments == 0 )
                {
                   
$display_text = T_( 'You must activate your account to see the comments.' );
                }
                elseif (
$number_of_comments == 1 )
                {
                   
$display_text = T_( 'There is <b>one comment</b> on this post but you must activate your account to see the comments.' );
                }
                else
                {
                   
$display_text = sprintf( T_( 'There are <b>%s comments</b> on this post but you must activate your account to see the comments.' ), $number_of_comments );
                }
                break;

            case
'registered':
               
// only registered users can see this post's comments
               
if( $number_of_comments == 0 )
                {
                   
$display_text = T_( 'You must be logged in to see the comments.' );
                }
                elseif (
$number_of_comments == 1 )
                {
                   
$display_text = T_( 'There is <b>one comment</b> on this post but you must be logged in to see the comments.' );
                }
                else
                {
                   
$display_text = sprintf( T_( 'There are <b>%s comments</b> on this post but you must be logged in to see the comments.' ), $number_of_comments );
                }
                break;

            case
'member':
               
// only members can see this post's comments
               
if( $number_of_comments == 0 )
                {
                   
$display_text = T_( 'You must be a member of this blog to see the comments.' );
                }
                elseif (
$number_of_comments == 1 )
                {
                   
$display_text = T_( 'There is one comment on this post but you must be a member of this blog to see the comments.' );
                }
                else
                {
                   
$display_text = sprintf( T_( 'There are %s comments on this post but you must be a member of this blog to see the comments.' ), $number_of_comments );
                }
                break;

            default:
               
// any is already handled, moderators shouldn't get any message
               
return false;
        }

        echo
'<div class="comment_posting_disabled_msg">';

        if( !
is_logged_in() )
        {
// user is not logged in at all
           
$redirect_to = $this->get_permanent_url().'#comments';
           
$login_link = '<a href="'.get_login_url( 'cannot see comments', $redirect_to ).'">'.T_( 'Log in now!' ).'</a>';
            echo
'<p>'.$display_text.' '.$login_link.'</p>';
            if(
$Settings->get( 'newusers_canregister' ) == 'yes' && $Settings->get( 'registration_is_public' ) )
            {
// needs to display register link
               
echo '<p>'.sprintf( T_( 'If you have no account yet, you can <a href="%s">register now</a>...<br />(It only takes a few seconds!)' ),
                           
get_user_register_url( $redirect_to, 'reg to see comments' ) ).'</p>';
            }
        }
        elseif(
$user_can_be_validated )
        {
// user is logged in but not activated
           
$activateinfo_link = '<a href="'.get_activate_info_url( $this->get_permanent_url().'#comments', '&amp;'  ).'">'.T_( 'More info &raquo;' ).'</a>';
            echo
'<p>'.$display_text.' '.$activateinfo_link.'</p>';
        }
        else
        {
// user is activated, but not allowed to view comments
           
echo $display_text;
        }

        echo
'</div>';

        return
false;
    }


   
/**
     * Template function: Check if user can leave comment on this post or display error
     *
     * @param string|NULL string to display before any error message; NULL to not display anything, but just return boolean
     * @param string string to display after any error message
     * @param string error message for non published posts, '#' for default
     * @param string error message for closed comments posts, '#' for default
     * @param string section title
     * @param array Skin params
     * @return boolean true if user can post, false if s/he cannot
     */
   
function can_comment( $before_error = '<p><em>', $after_error = '</em></p>', $non_published_msg = '#', $closed_msg = '#', $section_title = '', $params = array(), $comment_type = 'comment' )
    {
        global
$disp;

        if(
$comment_type == 'meta' && $this->can_meta_comment() )
        {    
// Meta comment are always allowed!
           
return true;
        }

        if(
$disp == 'terms' )
        {    
// Don't allow comment a page with terms & conditions:
           
return false;
        }

       
$display = ( ! is_null($before_error) );

        if(
$display )
        {
// display a comment form section even if comment form won't be displayed, "add new comment" links should point to this section
           
$comment_form_anchor = empty( $params['comment_form_anchor'] ) ? 'form_p' : $params['comment_form_anchor'];
            echo
'<a id="'.format_to_output( $comment_form_anchor.$this->ID, 'htmlattr' ).'"></a>';
        }

        if( !
$this->get_type_setting( 'use_comments' ) )
        {
// Comments are not allowed on this post by post type
           
return false;
        }

        if(
$this->check_blog_settings( 'allow_comments' ) )
        {
            if(
$this->get_type_setting( 'allow_disabling_comments' ) && ( $this->comment_status == 'disabled' ) )
            {
// Comments are disabled on this post
               
return false;
            }

            if(
$this->comment_status == 'closed' || $this->is_locked() )
            {
// Comments are closed on this post

               
if( $display)
                {
                    if(
$closed_msg == '#' )
                       
$closed_msg = T_( 'Comments are closed for this post.' );

                    echo
$before_error;
                    echo
$closed_msg;
                    echo
$after_error;
                }

                return
false;
            }

            if( (
$this->status == 'draft') || ($this->status == 'deprecated' ) || ($this->status == 'redirected' ) )
            {
// Post is not published

               
if( $display )
                {
                    if(
$non_published_msg == '#' )
                       
$non_published_msg = T_( 'This post is not published. You cannot leave comments.' );

                    echo
$before_error;
                    echo
$non_published_msg;
                    echo
$after_error;
                }

                return
false;
            }

            if(
is_logged_in() && ( $this->Blog->get( 'advanced_perms' ) ) && ! check_user_perm( 'blog_comment_statuses', 'create', false, $this->Blog->ID ) )
            {
// User doesn't have permission to create comments and advanced perms are enabled
               
if( $display )
                {
                    echo
$before_error;
                    echo
T_('You don\'t have permission to reply on this post.');
                    echo
$after_error;
                }
                return
false;
            }
            return
true; // OK, user can comment!
       
}

        if( (
$this->Blog->get_setting( 'allow_comments' ) != 'never' ) && $display )
        {
            if(
$this->comment_status == 'closed' || $this->comment_status == 'disabled' )
            {    
// Don't display the disabled comment form because we cannot create the comments for this post
               
return false;
            }
            echo
$section_title;
           
// set item_url for redirect after login, if login required
           
$item_url = $this->get_permanent_url().'#form_p'.$this->ID;
           
// display disabled comment form
           
echo_disabled_comments( $this->Blog->get_setting( 'allow_comments' ), $item_url, $params );
        }

       
// Current user not allowed to comment in this blog
       
return false;
    }


   
/**
     * Check if current User can see internal comments on this Item
     *
     * @return boolean
     */
   
function can_see_meta_comments()
    {
        if( !
is_logged_in() )
        {    
// User must be logged in
           
return false;
        }

        if( !
is_admin_page() )
        {    
// Check visibility of internal comments on front-office:
           
$item_Blog = & $this->get_Blog();
            if( !
$item_Blog || ! $item_Blog->get_setting( 'meta_comments_frontoffice' ) )
            {    
// Internal comments are disabled to be displayed on front-office for this Item's collection:
               
return false;
            }
        }

        return
check_user_perm( 'meta_comment', 'view', false, $this->get_blog_ID() );
    }


   
/**
     * Check if current User can leave internal comment on this Item
     *
     * @return boolean
     */
   
function can_meta_comment()
    {
        return
check_user_perm( 'meta_comment', 'add', false, $this->get_blog_ID() );
    }


   
/**
     * Check if current user is allowed for several action in this post's blog
     *
     * @private function
     *
     * @param string blog settings name. Param value can be 'allow_comments', 'allow_attachments','allow_rating_items'
     * @return boolean  true if user is allowed for the corresponding action
     */
   
function check_blog_settings( $settings_name, $settings_object = NULL )
    {
       
$this->load_Blog();

        if( (
$settings_name == 'allow_attachments' )
                && isset(
$settings_object )
                && (
$settings_object instanceof Comment )
                &&
$settings_object->is_meta()
                &&
$this->can_meta_comment() )
        {    
// Always allow attachments for meta Comments:
           
return true;
        }

        switch(
$this->Blog->get_setting( $settings_name ) )
        {
            case
'never':
                return
false;
            case
'any':
                return
true;
            case
'registered':
                return
is_logged_in( false );
            case
'member':
                return
check_user_perm( 'blog_ismember', 'view', false, $this->get_blog_ID(), false );
            case
'moderator':
                return
check_user_perm( 'blog_comments', 'edit', false, $this->get_blog_ID(), false );
            default:
               
debug_die( 'Invalid blog '.$settings_name.' settings!' );
        }

        return
false;
    }


   
/**
     * Template function: Check if user can attach files to this post comments
     *
     * @param boolean|integer ID of Temporary object to count also temporary attached files to new creating comment,
     *                        FALSE to count ONLY attachments of the created comments
     * @param string Comment type
     * @return boolean true if user can attach files to this post comments, false if s/he cannot
     */
   
function can_attach( $link_tmp_ID = false, $comment_type = 'comment' )
    {
        global
$Settings;

       
$attachments_quota_is_full = false;
        if(
is_logged_in() )
        {    
// We can check the attachments quota only for registered users
           
$this->load_Blog();

            if(
$comment_type == 'meta' && $this->can_meta_comment() )
            {    
// Always allow attachments for meta Comments:
               
return true;
            }

           
$max_attachments = (int)$this->Blog->get_setting( 'max_attachments' );
            if(
$max_attachments > 0 )
            {    
// Check attachments quota only when Blog setting "Max # of attachments" is defined
               
global $DB, $Session;

               
// Get a number of attachments for current user on this post
               
$link_tmp_ID = false;
               
$attachments_count = $this->get_attachments_number( NULL, $link_tmp_ID );

               
// Get the attachments from preview comment
               
global $checked_attachments;
                if( !empty(
$checked_attachments ) )
                {    
// Calculate also the attachments in the PREVIEW mode
                   
$attachments_count += count( explode( ',', $checked_attachments ) );
                }

                if(
$attachments_count >= $max_attachments )
                {    
// Current user already has max number of attachments on this post
                   
$attachments_quota_is_full = true;
                }
            }
        }

        return !
$attachments_quota_is_full && $this->check_blog_settings( 'allow_attachments' ) && $Settings->get( 'upload_enabled' );
    }


   
/**
     * Check if the post contains inline file placeholders without corresponding attachment file.
     * Removes the invalid inline file placeholders from the item content.
     *
     * @param string Content
     * @return string Prepared content
     */
   
function check_and_clear_inline_files( $content )
    {
       
preg_match_all( '/\[(image|file|inline|video|audio|thumbnail|folder):(\d+):?[^\]]*\]/i', $content, $inline_images );

        if( empty(
$inline_images[1] ) )
        {
// There are no inline image placeholders in the post content
           
return $content;
        }

       
// There are inline image placeholders in the item's content:
       
global $DB;
       
$links_SQL = new SQL( 'Get item links IDs of the inline files' );
       
$links_SQL->SELECT( 'link_ID' );
       
$links_SQL->FROM( 'T_links' );
        if( empty(
$this->ID ) )
        {    
// Preview mode for new creating item:
           
$links_SQL->WHERE( 'link_tmp_ID = '.$DB->quote( param( 'temp_link_owner_ID', 'integer', 0 ) ) );
        }
        else
        {    
// Normal mode for existing Item in DB:
           
$links_SQL->WHERE( 'link_itm_ID = '.$DB->quote( $this->ID ) );
        }
       
$inline_links_IDs = $DB->get_col( $links_SQL );

       
$unused_inline_images = array();
        foreach(
$inline_images[2] as $i => $inline_link_ID )
        {
            if( !
in_array( $inline_link_ID, $inline_links_IDs ) )
            {
// This inline image must be removed from content
               
$unused_inline_images[] = $inline_images[0][ $i ];
            }
        }

       
// Clear the unused inline images from content:
       
if( count( $unused_inline_images ) )
        {    
// Remove all unused inline images from the content:
           
global $Messages;
           
$unused_inline_images = array_unique( $unused_inline_images );
           
$content = replace_outside_code_tags( $unused_inline_images, '', $content, 'replace_content', 'str' );
           
$Messages->add( T_('Invalid inline file placeholders won\'t be displayed.'), 'note' );
        }

        return
$content;
    }


   
/**
     * Get a number of attachments on this post
     *
     * @param object User
     * @param boolean|integer ID of Temporary object to count also temporary attached files to new creating comment,
     *                        FALSE to count ONLY attachments of the created comments
     * @return integer Number of attachments
     */
   
function get_attachments_number( $User = NULL, $link_tmp_ID = false )
    {
        global
$DB, $cache_item_attachments_number;

        if(
is_null( $User ) )
        {    
// Use current user by default
           
global $current_User;
           
$User = $current_User;
        }

        if( !isset(
$cache_item_attachments_number ) )
        {    
// Init cache variable at first time
           
$cache_item_attachments_number = array();
        }

        if( isset(
$cache_item_attachments_number[ $User->ID ][ $link_tmp_ID ] ) )
        {    
// Get a number of attachments from cache variable:
           
return $cache_item_attachments_number[ $User->ID ][ $link_tmp_ID ];
        }

       
// Get a number of attachments from DB:
       
$SQL = new SQL( 'Get a number of comment attachments per item #'.$this->ID.' by user #'.$User->ID );
       
$SQL->SELECT( 'COUNT( link_ID )' );
       
$SQL->FROM( 'T_links' );
       
$SQL->FROM_add( 'LEFT JOIN T_comments ON comment_ID = link_cmt_ID' );
       
$SQL->WHERE( 'link_creator_user_ID = '.$DB->quote( $User->ID ) );
       
$sql_where = 'comment_item_ID = '.$DB->quote( $this->ID );
        if(
$link_tmp_ID )
        {    
// Count also the attached files to new creating comment:
           
$sql_where .= ' OR ( comment_item_ID IS NULL AND link_tmp_ID = '.$DB->quote( $link_tmp_ID ).' )';
        }
       
$SQL->WHERE_and( $sql_where );
       
// Do not include meta comments in the count:
       
$SQL->WHERE_and( 'comment_type != "meta"');
       
$cache_item_attachments_number[ $User->ID ][ $link_tmp_ID ] = intval( $DB->get_var( $SQL->get(), 0, NULL, $SQL->title ) );

        return
$cache_item_attachments_number[ $User->ID ][ $link_tmp_ID ];
    }


   
/**
     * Get how much files user can attach on this post yet
     *
     * @param object User
     * @return integer|string Number of files which current user can attach to this post | 'unlimit'
     */
   
function get_attachments_limit( $User = NULL )
    {
        if(
is_logged_in() )
        {    
// We can check the attachments quota only for registered users
           
$this->load_Blog();
           
$max_attachments = (int)$this->Blog->get_setting( 'max_attachments' );
            if(
$max_attachments > 0 )
            {    
// Get a limit only when Blog setting "Max # of attachments" is defined
               
return $max_attachments - $this->get_attachments_number( $User );
            }
        }

        return
'unlimit';
    }


   
/**
     * Duplicate attachments from another Item
     *
     * @param integer Item ID
     */
   
function duplicate_attachments( $item_ID )
    {
        global
$DB;

       
// Initiliaze Link Owner where we create a temporary object for new creating Item:
       
$LinkOwner = new LinkItem( $this, param( 'temp_link_owner_ID', 'integer', 0 ) );

        if( ! empty(
$LinkOwner->link_Object->tmp_ID ) )
        {    
// If temporaty object has been created for new Item:
           
$DB->query( 'INSERT INTO T_links ( link_datecreated, link_datemodified, link_creator_user_ID, link_lastedit_user_ID, link_tmp_ID, link_file_ID, link_position, link_order )
                SELECT link_datecreated, link_datemodified, link_creator_user_ID, link_lastedit_user_ID, '
.$DB->quote( $LinkOwner->link_Object->tmp_ID ).', link_file_ID, link_position, link_order
                  FROM T_links
                WHERE link_itm_ID = '
.$DB->quote( $item_ID ) );
        }
    }


   
/**
     * Template function: Check if user can rate this post
     *
     * @return boolean true if user can post, false if s/he cannot
     */
   
function can_rate()
    {
        return
$this->check_blog_settings( 'allow_rating_items' );
    }


   
/**
     * Get the prerendered content. If it has not been generated yet, it will.
     *
     * NOTE: This calls {@link Item::dbupdate()}, if renderers get changed (from Plugin hook).
     *       (not for preview though)
     *
     * @param string Format, see {@link format_to_output()}.
     *        Only "htmlbody", "entityencoded", "xml" and "text" get cached.
     * @return string
     */
   
function get_prerendered_content( $format )
    {
        global
$Plugins;
        global
$preview;

        if(
$preview )
        {
           
$this->update_renderers_from_Plugins();
           
$post_renderers = $this->get_renderers_validated();

           
// Call RENDERER plugins:
           
$r = $this->content;
           
$Plugins->render( $r /* by ref */, $post_renderers, $format, array( 'Item' => $this ), 'Render' );

           
// Check and clear inline files, to avoid to have placeholders without corresponding attachment
           
$r = $this->check_and_clear_inline_files( $r );

            if(
$this->is_intro() || ! $this->get_type_setting( 'allow_breaks' ) )
            {    
// Don't use the content separators for intro items and if it is disabled by item type:
               
$r = replace_outside_code_tags( array( '[teaserbreak]', '[pagebreak]' ), '', $r, 'replace_content', 'str' );
            }

            return
$r;
        }


       
$r = null;

       
$post_renderers = $this->get_renderers_validated();
       
$cache_key = $format.'/'.implode('.', $post_renderers); // logic gets used below, for setting cache, too.

       
$use_cache = $this->ID && in_array( $format, array('htmlbody', 'entityencoded', 'xml', 'text') )
                && !
$this->is_revision(); // do not use cache when viewing historical revision

        // $use_cache = false;

       
if( $use_cache )
        {
// the format/item can be cached:
           
$ItemPrerenderingCache = & get_ItemPrerenderingCache();

            if( isset(
$ItemPrerenderingCache[$format][$this->ID][$cache_key]) )
            {
// already in PHP cache.
               
$r = $ItemPrerenderingCache[$format][$this->ID][$cache_key];
               
// Save memory, typically only accessed once.
               
unset($ItemPrerenderingCache[$format][$this->ID][$cache_key]);
            }
            else
            {    
// Try loading from DB cache, including all items in MainList/ItemList.
               
global $DB;

                if( ! isset(
$ItemPrerenderingCache[$format]) )
                {
// only do the prefetch loading once.
                   
$prefetch_IDs = $this->get_prefetch_itemlist_IDs();

                   
// Load prerendered content for all items in MainList/ItemList.
                    // We load the current $format only, since it's most likely that only one gets used.
                   
$ItemPrerenderingCache[$format] = array();

                   
$rows = $DB->get_results( "
                        SELECT itpr_itm_ID, itpr_format, itpr_renderers, itpr_content_prerendered
                            FROM T_items__prerendering
                         WHERE itpr_itm_ID IN ("
.$DB->quote( $prefetch_IDs ).")
                             AND itpr_format = '"
.$format."'",
                             
OBJECT, 'Preload prerendered item content for MainList/ItemList ('.$format.')' );
                    foreach(
$rows as $row)
                    {
                       
$row_cache_key = $row->itpr_format.'/'.$row->itpr_renderers;

                        if( ! isset(
$ItemPrerenderingCache[$format][$row->itpr_itm_ID]) )
                        {
// init list
                           
$ItemPrerenderingCache[$format][$row->itpr_itm_ID] = array();
                        }

                       
$ItemPrerenderingCache[$format][$row->itpr_itm_ID][$row_cache_key] = $row->itpr_content_prerendered;
                    }

                   
// Set the value for current Item.
                   
if( isset($ItemPrerenderingCache[$format][$this->ID][$cache_key]) )
                    {
                       
$r = $ItemPrerenderingCache[$format][$this->ID][$cache_key];
                       
// Save memory, typically only accessed once.
                       
unset($ItemPrerenderingCache[$format][$this->ID][$cache_key]);
                    }
                }
                else
                {
// This item has not been fetched by the initial prefetch query; only get this item.
                    // dh> This is quite unlikely to happen, but you never know.
                    // This gets not added to ItemPrerenderingCache, since it would only waste
                    // memory - an item gets typically only accessed once per page, and even if
                    // it would get accessed more often, there is a cache higher in the chain
                    // ($this->content_pages).
                   
$cache = $DB->get_var( "
                        SELECT itpr_content_prerendered
                            FROM T_items__prerendering
                         WHERE itpr_itm_ID = "
.$this->ID."
                             AND itpr_format = '"
.$format."'
                             AND itpr_renderers = '"
.implode('.', $post_renderers)."'", 0, 0, 'Check prerendered item content' );
                    if(
$cache !== NULL ) // may be empty string
                   
{ // Retrieved from cache:
                        // echo ' retrieved from prerendered cache';
                       
$r = $cache;
                    }
                }
            }
        }

        if( ! isset(
$r ) )
        {
// Not cached yet:
           
global $Debuglog;

            if(
$this->update_renderers_from_Plugins() )
            {
               
$post_renderers = $this->get_renderers_validated(); // might have changed from call above
               
$cache_key = $format.'/'.implode('.', $post_renderers);

               
// Save new renderers with item:
               
$this->dbupdate();
            }

           
// Call RENDERER plugins:
           
$r = $this->get( 'content' );
           
$Plugins->render( $r /* by ref */, $post_renderers, $format, array( 'Item' => $this ), 'Render' );

           
// Check and clear inline files, to avoid to have placeholders without corresponding attachment
           
$r = $this->check_and_clear_inline_files( $r );

            if(
$this->is_intro() || ! $this->get_type_setting( 'allow_breaks' ) )
            {    
// Don't use the content separators for intro items and if it is disabled by item type:
               
$r = replace_outside_code_tags( array( '[teaserbreak]', '[pagebreak]' ), '', $r, 'replace_content', 'str' );
            }

           
$Debuglog->add( 'Generated pre-rendered content ['.$cache_key.'] for item #'.$this->ID, 'items' );

            if(
$use_cache )
            {
// save into DB (using REPLACE INTO because it may have been pre-rendered by another thread since the SELECT above)
               
global $servertimenow;
               
$DB->query( 'REPLACE INTO T_items__prerendering ( itpr_itm_ID, itpr_format, itpr_renderers, itpr_content_prerendered, itpr_datemodified )
                    VALUES ( '
.$this->ID.', '.$DB->quote( $format ).', '.$DB->quote( implode( '.', $post_renderers ) ).', '.$DB->quote( $r ).', '.$DB->quote( date2mysql( $servertimenow ) ).' )', 'Cache prerendered item content' );
            }
        }

        return
$r;
    }


   
/**
     * Unset any prerendered content for this item (in PHP cache).
     */
   
function delete_prerendered_content()
    {
        global
$DB;

       
// Delete DB rows.
       
$DB->query( 'DELETE FROM T_items__prerendering WHERE itpr_itm_ID = '.$this->ID );

       
// Delete cache.
       
$ItemPrerenderingCache = & get_ItemPrerenderingCache();
        foreach(
array_keys($ItemPrerenderingCache) as $format )
        {
            unset(
$ItemPrerenderingCache[$format][$this->ID]);
        }

       
// Delete derived properties.
       
unset($this->content_pages);
    }


   
/**
     * Trigger {@link Plugin::ItemApplyAsRenderer()} event and adjust renderers according
     * to return value.
     * @return boolean True if renderers got changed.
     */
   
function update_renderers_from_Plugins()
    {
        global
$Plugins;

       
$r = false;

        if( !isset(
$Plugins) )
        {    
// This can happen in maintenance modules running with minimal init, during install, or in tests.
           
return $r;
        }

        foreach(
$Plugins->get_list_by_event('ItemApplyAsRenderer') as $Plugin )
        {
            if( empty(
$Plugin->code) )
                continue;

           
$tmp_params = array( 'Item' => & $this );
           
$plugin_r = $Plugin->ItemApplyAsRenderer( $tmp_params );

            if(
is_bool($plugin_r) )
            {
                if(
$plugin_r )
                {
                   
$r = $this->add_renderer( $Plugin->code ) || $r;
                }
                else
                {
                   
$r = $this->remove_renderer( $Plugin->code ) || $r;
                }
            }
        }

        return
$r;
    }


   
/**
     * Display excerpt of an item.
     * @param array Associative list of params
     *   - before
     *   - after
     *   - excerpt_before_more
     *   - excerpt_after_more
     *   - excerpt_more_text
     *   - format
     */
   
function excerpt( $params = array() )
    {
       
// Make sure we are not missing any param:
       
$params = array_merge( array(
               
'before'              => '<div class="excerpt">',
               
'after'               => '</div>',
               
'excerpt_before_more' => ' <span class="excerpt_more">',
               
'excerpt_after_more'  => '</span>',
               
'excerpt_more_text'   => '#more+arrow',     // possible special values: ...
               
'excerpt_more_class'  => 'nowrap',
               
'format'              => 'htmlbody',
            ),
$params );

       
$r = $this->get_excerpt( $params['format'] );

        if( ! empty(
$r ) )
        {
            echo
$params['before'];
           
            if( isset(
$params['max_words'] ) )
            {    
               
// to stop displaying double hellip
               
$params['avoid_end_hellip'] = true;
               
                echo
excerpt_words( $r, $params['max_words'], $params );
            }
            else
            {
                echo
$r;
            }

            if( ! isset(
$params['excerpt_no_more_link'] ) )
            {
               
$this->permanent_link( array(
                       
'before'      => $params['excerpt_before_more'],
                       
'after'       => $params['excerpt_after_more'],
                       
'text'        => $params['excerpt_more_text'],
                       
'title'       => '#',
                       
'class'       => $params['excerpt_more_class'],
                    ) );
            }

            echo
$params['after'];
        }
    }


   
/**
     * Template tag: get excerpt 2 (Full version)
     * This full version may auto-generate an excerpt if it is found to be empty.
     *
     * @deprecated Use $this->get_excerpt() instead.
     *
     * @param array DEPRECATED: Associative list of params
     *   - allow_empty: DEPRECATED force generation if excert is empty (Default: false)
     *   - update_db: DEPRECATED update the DB if we generated an excerpt (Default: true)
     * @return string
     */
   
function get_excerpt2( $params = array() )
    {
        return
$this->get_excerpt();
    }


   
/**
     * Make sure, the pages have been obtained (and split up_ from prerendered cache.
     *
     * @param string Format, used to retrieve the matching cache; see {@link format_to_output()}
     */
   
function split_pages( $format = 'htmlbody' )
    {
        if( ! isset(
$this->content_pages[ $format ] ) )
        {
           
// SPLIT PAGES:
           
$this->content_pages[ $format ] = split_outcode( '[pagebreak]', $this->get_prerendered_content( $format ) );

           
// Balance HTML tags
           
$this->content_pages[ $format ] = array_map( 'balance_tags', $this->content_pages[ $format ] );

           
$this->pages = count( $this->content_pages[ $format ] );
        }
    }


   
/**
     * Get a specific page to display (from the prerendered cache)
     *
     * @param integer Page number, NULL/"#" for current
     * @param string Format, used to retrieve the matching cache; see {@link format_to_output()}
     */
   
function get_content_page( $page = NULL, $format = 'htmlbody' )
    {
        global
$preview;

       
// Get requested content page:
       
if( ! isset($page) || $page === '#' )
        {
// We want to display the page requested by the user:
           
$page = isset( $GLOBALS['page'] ) ? $GLOBALS['page'] : 1;
        }

       
// Make sure, the pages are split up:
       
$this->split_pages( $format );

       
$content_page = '';

        if(
$preview && $this->pages > 1 && ! $this->ID )
        {
// This is a preview of an unsaved  multipage item
           
foreach( $this->content_pages[$format] as $page => $page_content )
            {
                if(
$page !== 0 )
                {
                   
$content_page .= '<span class="badge badge-info">Page '.( $page + 1 ).'</span>';
                }

               
$content_page .= $page_content;
            }
        }
        else
        {
            if(
$page < 1 )
            {
               
$page = 1;
            }

            if(
$page > $this->pages )
            {
               
$page = $this->pages;
            }

           
$content_page = $this->content_pages[$format][$page-1];
        }

        if( !
check_user_perm( 'item_post!CURSTATUS', 'edit', false, $this ) )
        {    
// Clean up rendering errors from content if current User has no permission to edit this Item:
           
$content_page = clear_rendering_errors( $content_page );
        }

        return
$content_page;
    }


   
/**
     * Display content teaser of item (will stop at "[teaserbreak]"
     */
   
function content_teaser( $params )
    {
       
// Make sure we are not missing any param:
       
$params = array_merge( array(
               
'before'      => '',
               
'after'       => '',
               
'disppage'    => '#',
               
'stripteaser' => '#',
               
'format'      => 'htmlbody',
            ),
$params );

       
$r = $this->get_content_teaser( $params['disppage'], $params['stripteaser'], $params['format'], $params );

        if( ! empty(
$r ) )
        {
            echo
$params['before'];
            echo
$r;
            echo
$params['after'];
        }
    }

   
/**
     * Template function: get content teaser of item (will stop at "[teaserbreak]")
     *
     * @param mixed page number to display specific page, # for url parameter
     * @param boolean # if you don't want to repeat teaser after more link was pressed and <-- noteaser --> has been found
     * @param string filename to use to display more
     * @param array Params
     * @return string
     */
   
function get_content_teaser( $disppage = '#', $stripteaser = '#', $format = 'htmlbody', $params = array() )
    {
        global
$more;

       
$params = array_merge( $params, array(
               
'disppage' => $disppage,
               
'dispmore' => ( $more != 0 ),
               
'format'   => $format,
            ) );

       
$params['view_type'] = 'full';
        if(
$this->has_content_parts( $params ) )
        {
// This is an extended post (has a more section):
           
if( $stripteaser === '#' )
            {
               
// If we're in "more" mode and we want to strip the teaser, we'll strip:
               
$stripteaser = ( $more && $this->get_setting( 'hide_teaser' ) );
            }

            if(
$stripteaser )
            {
                return
NULL;
            }
           
$params['view_type'] = 'teaser';
        }

       
$content_parts = $this->get_content_parts( $params );
       
$output = array_shift( $content_parts );

       
// Render content by plugins and inline short tags at display time:
       
$output = $this->get_rendered_content( $output, $params );

        return
$output;
    }


   
/**
     * Get rendered content by plugins and inline short tags at display time
     *
     * @param string Source content
     * @param array Params
     * @return string Rendered content
     */
   
function get_rendered_content( $content, $params = array() )
    {
        global
$Plugins, $preview;

       
$params = array_merge( array(
               
'format'    => 'htmlbody',
               
'dispmore'  => false,
               
'view_type' => 'full',
            ),
$params );

       
// Render all inline tags to HTML code:
       
$output = $this->render_inline_tags( $content, $params );

       
// Render switchable content:
       
$output = $this->render_switchable_content( $output );

       
// Trigger Display plugins FOR THE STUFF THAT WOULD NOT BE PRERENDERED:
       
$output = $Plugins->render( $output, $this->get_renderers_validated(), $params['format'], array(
               
'Item'      => $this,
               
'preview'   => $preview,
               
'dispmore'  => $params['dispmore'],
               
'view_type' => $params['view_type'],
            ),
'Display' );

       
// Character conversions:
       
if( stristr( $output, '<script' ) !== false )
        {    
// Format content on everything outside <script>:
            // E.g.: to avoid replacing of condition operator from & to &amp;
           
$output = callback_on_non_matching_blocks( $output,
               
'~<(script)[^>]*>.*?</\1>~is',
               
'format_to_output', array( $params['format'] ) );
        }

        return
$output;
    }


   
/**
     * Get content parts (split by "[teaserbreak]").
     *
     * @param array 'disppage', 'format'
     * @return array Array of content parts
     */
   
function get_content_parts( $params )
    {
       
// Make sure we are not missing any param:
       
$params = array_merge( array(
               
'disppage' => '#',
               
'format'   => 'htmlbody',
            ),
$params );

       
$content_page = $this->get_content_page( $params['disppage'], $params['format'] ); // cannot include format_to_output() because of the magic below.. eg '[teaserbreak]' will get stripped in "xml"

       
$content_parts = split_outcode( '[teaserbreak]', $content_page );

       
// Balance HTML tags
       
$content_parts = array_map( 'balance_tags', $content_parts );

        return
$content_parts;
    }


   
/**
     * Get full content with teaser and extension and all pages
     *
     * @param string Format
     * @param array Params
     * @return string Content
     */
   
function get_full_content( $format = 'htmlbody', $params = array() )
    {
       
$params = array_merge( $params, array(
               
'dispmore'  => true,
               
'view_type' => 'full',
               
'format'    => $format,
            ) );

       
$output = '';
       
$this->split_pages( $format );
        foreach(
$this->content_pages[ $format ] as $p => $content_page )
        {
           
$content_parts = $this->get_content_parts( array_merge( $params, array( 'disppage' => $p + 1 ) ) );

           
$output .= implode( "\n\n", $content_parts );
        }

       
// Render content by plugins and inline short tags at display time:
       
$output = $this->get_rendered_content( $output, $params );

        return
$output;
    }


   
/**
     * DEPRECATED
     */
   
function content()
    {
       
// ---------------------- POST CONTENT INCLUDED HERE ----------------------
       
skin_include( '_item_content.inc.php', array(
               
'image_size' => 'fit-400x320',
            ) );
       
// Note: You can customize the default item feedback by copying the generic
        // /skins/_item_feedback.inc.php file into the current skin folder.
        // -------------------------- END OF POST CONTENT -------------------------
   
}


   
/**
     * Display content extension of item (part after "[teaserbreak]")
     */
   
function content_extension( $params )
    {
       
// Make sure we are not missing any param:
       
$params = array_merge( array(
               
'before'      => '',
               
'after'       => '',
               
'disppage'    => '#',
               
'format'      => 'htmlbody',
               
'force_more'  => false,
               
'image_size'  => 'fit-400x320',
            ),
$params );

       
$r = $this->get_content_extension( $params['disppage'], $params['force_more'], $params['format'], $params );

        if( ! empty(
$r ) )
        {
            echo
$params['before'];
            echo
$r;
            echo
$params['after'];
        }
    }


   
/**
     * Template function: get content extension of item (part after "[teaserbreak]")
     *
     * @param mixed page number to display specific page, # for url parameter
     * @param boolean
     * @param string filename to use to display more
     * @param array additional params passthrough
     * @return string
     */
   
function get_content_extension( $disppage = '#', $force_more = false, $format = 'htmlbody', $params = array() )
    {
        global
$more;

        if( !
$more && ! $force_more )
        {    
// NOT in more mode:
           
return NULL;
        }

       
// Set default params
       
$params = array_merge( array(
               
'image_size' => 'fit-400x320',
            ),
$params );

       
// Don't rewrite these params from array $params, Use them from separate params of this function
       
$params = array_merge( $params, array(
               
'disppage'  => $disppage,
               
'dispmore'  => true,
               
'view_type' => 'extension',
               
'format'    => $format,
            ) );

        if( !
$this->has_content_parts( $params ) )
        {
// This is NOT an extended post
           
return NULL;
        }

       
$content_parts = $this->get_content_parts( $params );

       
// Output everything after [teaserbreak]:
       
array_shift( $content_parts );
       
$output = implode( '', $content_parts );

       
// Render content by plugins and inline short tags at display time:
       
$output = $this->get_rendered_content( $output, $params );

        return
$output;
    }


   
/**
     * Increase view counter
     *
     * @deprecated since 5.1.0-beta
     */
   
function count_view( $params = array() )
    {
       
// We always return false, since counting views feature was removed.
       
return false;
    }


   
/**
     * Get all custom fields definitions of this Item with once loading them into cache array $this->custom_fields
     *
     * @return array Custom fields, each array item is array with keys: ID, ityp_ID, label, name, type, order, note, value.
     */
   
function get_custom_fields_defs()
    {
        if( ! isset(
$this->custom_fields ) ||
            ! isset(
$this->custom_fields_loaded_ityp_ID ) ||
           
$this->custom_fields_loaded_ityp_ID != $this->get( 'ityp_ID' ) )
        {    
// Load item custom fields only once if Item Type was not changed:
           
global $DB;

           
$SQL = new SQL( 'Load all custom fields definitions of Item Type #'.$this->get( 'ityp_ID' ).' with values for Item #'.$this->ID );
           
$SQL->SELECT( 'T_items__type_custom_field.*' );
           
$SQL->FROM( 'T_items__type_custom_field' );
            if(
$DB->get_var( 'SHOW TABLES LIKE "T_items__item_custom_field"' ) !== NULL )
            {    
// New version:
               
$SQL->SELECT_add( ', icfv_value, IFNULL( icfv_parent_sync, 1 ) AS icfv_parent_sync' );
               
$SQL->FROM_add( 'LEFT JOIN T_items__item_custom_field ON itcf_name = icfv_itcf_name AND icfv_item_ID = '.$this->ID );
            }
            else
            {    
// Old version < 15280, used on upgrade blocks by function Item->insert():
               
$SQL->SELECT_add( ', iset_value' );
               
$SQL->FROM_add( 'LEFT JOIN T_items__item_settings ON iset_name = CONCAT( "custom:", itcf_name ) AND iset_item_ID = '.$this->ID );
            }
           
$SQL->WHERE_and( 'itcf_ityp_ID = '.$DB->quote( $this->get( 'ityp_ID' ) ) );
           
$SQL->ORDER_BY( 'itcf_order, itcf_ID' );
           
$custom_fields = $DB->get_results( $SQL, ARRAY_A );

           
$this->custom_fields = array();
            foreach(
$custom_fields as $custom_field )
            {    
// Use field name/code as key/index of array:
               
$this->custom_fields[ $custom_field['itcf_name'] ] = array();
                foreach(
$custom_field as $custom_field_key => $custom_field_value )
                {
                   
$this->custom_fields[ $custom_field['itcf_name'] ][ substr( $custom_field_key, 5 ) ] = $custom_field_value;
                }
            }
           
// Store current Item Type in order to reload the custom fields when Item Type was changed:
           
$this->custom_fields_loaded_ityp_ID = $this->get( 'ityp_ID' );
        }

        return
$this->custom_fields;
    }


   
/**
     * Set item custom field value or parent_sync
     *
     * @param string Field name
     * @param string New value
     * @param string Value key: 'value', 'parent_sync'
     * @param boolean TRUE to set to NULL if empty value
     */
   
function set_custom_field( $field_name, $new_value, $value_key = 'value', $make_null = true )
    {
        if(
$value_key != 'value' && $value_key != 'parent_sync' )
        {    
// Skip unknown column in the table T_items__type_custom_field:
           
return;
        }

       
// Load all custom fields for this item:
       
$this->get_custom_fields_defs();

        if( ! isset(
$this->custom_fields[ $field_name ] ) )
        {    
// Set new array for custom field data, Used for new creating Item:
           
$this->custom_fields[ $field_name ] = array();
        }

        if(
$value_key == 'value' && $make_null && empty( $new_value ) )
        {    
// Set NULL for empty value:
           
$new_value = NULL;
        }

       
// Set new value for the field:
       
$this->custom_fields[ $field_name ][ $value_key ] = $new_value;
    }


   
/**
     * Update custom fields
     *
     * @return boolean TRUE if custom fields were updated
     */
   
function update_custom_fields()
    {
        global
$DB;

        if( empty(
$this->ID ) )
        {    
// Item must be stored in DB
           
return false;
        }

        if(
$DB->get_var( 'SHOW TABLES LIKE "T_items__item_custom_field"' ) === NULL )
        {    
// Skip because T_items__item_custom_field doesn't exist in DB on old versions < 15280:
           
return false;
        }

       
// Get all custom fields:
       
$custom_fields = $this->get_custom_fields_defs();

       
// Remove old values from DB:
       
$deleted_cf_num = $DB->query( 'DELETE FROM T_items__item_custom_field
            WHERE icfv_item_ID = '
.$this->ID,
           
'Delete old custom field values before insert new values for Item #'.$this->ID );

        if( empty(
$custom_fields ) )
        {    
// No new custom fields to update:
           
return ( $deleted_cf_num > 0 );
        }

       
// Insert new values:
       
$cf_insert_data = array();
        foreach(
$custom_fields as $custom_field_name => $custom_field )
        {
           
$cf_insert_data[] = '( '.$this->ID.', '
               
.$DB->quote( $custom_field_name ).', '
               
.$DB->quote( $custom_field['value'] ).', '
               
.$DB->quote( isset( $custom_field['parent_sync'] ) && $custom_field['parent_sync'] !== NULL ? $custom_field['parent_sync'] : 1 ).' )';
        }
       
$inserted_cf_num = $DB->query( 'INSERT INTO T_items__item_custom_field ( icfv_item_ID, icfv_itcf_name, icfv_value, icfv_parent_sync )
            VALUES '
.implode( ', ', $cf_insert_data ),
           
'Insert new custom field values for Item #'.$this->ID );

        return (
$deleted_cf_num > 0 || $inserted_cf_num > 0 );
    }


   
/**
     * Get item custom field label/title by field name
     *
     * @param string Field name, see {@link get_custom_fields_defs()}
     * @return string|boolean FALSE if the field doesn't exist
     */
   
function get_custom_field_title( $field_name )
    {
       
// Get all custom fields by item ID:
       
$custom_fields = $this->get_custom_fields_defs();

        if( ! isset(
$custom_fields[ $field_name ] ) )
        {    
// The requested field is not detected:
           
return false;
        }

        return
$custom_fields[ $field_name ]['label'];
    }


   
/**
     * Get item custom field value by field name
     *
     * @param string Field name, see {@link load_custom_field_value()}
     * @param string Restring field by type, FALSE - to don't restrict
     * @return mixed false if the field doesn't exist Double/String otherwise depending from the custom field type
     */
   
function get_custom_field_value( $field_name, $restrict_type = false )
    {
       
// Get all custom fields by item ID:
       
$custom_fields = $this->get_custom_fields_defs();

        if( ! isset(
$custom_fields[ $field_name ] ) )
        {    
// The requested field is not detected:
           
return false;
        }

        if(
$restrict_type !== false && $custom_fields[ $field_name ]['type'] != $restrict_type )
        {    
// The requested field is detected but it has another type:
           
return false;
        }

       
// Get custom item field value:
       
if( $this->is_revision() )
        {    
// from current revision if it is active for this Item:
           
return $this->get_revision_custom_field_value( $field_name );
        }
        else
        {    
// from the item setting:
           
return $custom_fields[ $field_name ]['value'];
        }
    }


   
/**
     * Get formatted item custom field value by field name
     *
     * @param string Field name, see {@link get_custom_fields_defs()}
     * @param array Params
     * @return string|boolean FALSE if the field doesn't exist
     */
   
function get_custom_field_formatted( $field_name, $params = array() )
    {
       
$params = array_merge( array(
               
'field_value_format'  => '', // Format for custom field, Leave empty to use a format from DB
               
'field_restrict_type' => false, // Restrict field by type(double, varchar, html, text, url, image, computed, separator), FALSE - to don't restrict
               
'expansion'           => 'default', // 'default': || = '<br />', | | = space; 'vertical': both = '<br />'; 'horizontal': both = space.
           
), $params );

       
// Try to get an original value of the requested custom field:
       
$custom_field_value = $this->get_custom_field_value( $field_name, $params['field_restrict_type'] );

        if(
$custom_field_value === false )
        {    
// The requested field is not found for the item type:
           
return false;
        }

       
$orig_custom_field_value = $custom_field_value;

       
// Get custom field:
       
$custom_fields = $this->get_custom_fields_defs();
       
$custom_field = $custom_fields[ $field_name ];

        if( (
$custom_field_value === '' || $custom_field_value === NULL ) && // don't format empty value
           
! in_array( $custom_field['type'], array( 'double', 'computed', 'url' ) ) ) // double, computed and url fields may have a special format even for empty value
       
{    // Don't format value in such cases:
           
return $custom_field_value;
        }

        if(
$params['field_value_format'] === '' )
        {    
// Use a format from DB:
           
$format = $custom_field['format'];
        }
        else
        {    
// Use a format from params:
           
$format = $params['field_value_format'];
        }

        switch(
$custom_field['type'] )
        {
            case
'double':
            case
'computed':
               
// Format double/computed field value:
               
if( $format === NULL || $format === '' )
                {    
// No format:
                   
break;
                }

               
$formats = explode( ';', $format );

                if(
count( $formats ) > 4 )
                {    
// Check formats like 123=text:
                   
for( $f = 4; $f < count( $formats ); $f++ )
                    {
                        if(
strpos( $formats[ $f ], '=' ) !== false )
                        {    
// If format contains the equal sign
                           
$cur_format = explode( '=', $formats[ $f ], 2 );
                            if(
$cur_format[0] == $custom_field_value )
                            {    
// Use the searched format for given value:
                               
$custom_field_value = isset( $cur_format[1] ) ? $cur_format[1] : $custom_field_value;
                               
// Stop here to don't apply other format:
                               
break 2;
                            }
                        }
                    }
                }

                if(
$custom_field_value === '' || $custom_field_value === NULL )
                {    
// If value is empty string or NULL
                   
if( empty( $formats[3] ) )
                    {    
// Use default for empty values:
                       
$custom_field_value = /* TRANS: "Not Available" */ '{'.T_('N/A').'}';
                    }
                    else
                    {    
// Use a special format for empty values:
                       
$custom_field_value = $formats[3];
                    }
                   
// Stop here to don't apply other format:
                   
break;
                }

                if(
$custom_field_value == 0 && isset( $formats[2] ) )
                {    
// If value == 0
                   
$custom_field_value = $formats[2];
                   
// Stop here to don't apply other format:
                   
break;
                }

               
// Format all other values which are not related to the formats above:
               
$format = $formats[0];
                if(
$custom_field_value < 0 && ! empty( $formats[1] ) )
                {    
// Use a format for negative values:
                   
$custom_field_value = abs( $custom_field_value );
                   
$format = $formats[1];
                }

                if(
in_array( $format, array( '#yes#', '(yes)', '#no#', '(no)', '(+)', '(-)', '(!)', '||', '| |' ) ) ||
                   
strpos( $format, '#stars' ) !== false ||
                    (
$format !== '' && ! preg_match( '/\d/', $format ) ) )
                {    
// Use special formats:
                   
$custom_field_value = $format;
                    break;
                }

               
// Format number:
               
if( preg_match( '#^(.+?)\[\.([a-z0-9\-_\.]+)\]$#i', $format, $format_class ) )
                {    
// Format has a class:
                   
$format = $format_class[1];
                   
$format_class = str_replace( '.', ' ', $format_class[2] );
                }
                else
                {    
// No class for the format:
                   
$format_class = '';
                }
               
$format = preg_split( '#(\d+)#', $format, -1, PREG_SPLIT_DELIM_CAPTURE );
               
$f_num = count( $format );
               
$format_decimals = 0;
               
$format_dec_point = '.';
               
$format_thousands_sep = '';
               
$format_prefix = isset( $format[0] ) ? $format[0] : '';
               
$format_suffix = $f_num > 1 ? $format[ $f_num - 1 ] : '';
                if(
$f_num > 2 )
                {    
// Extract data for number fomatting:
                   
if( in_array( $format_suffix, array( '.', ',' ) ) &&
                        isset(
$format[2] ) &&
                       
in_array( $format[2], array( '.', ',' ) ) )
                    {    
// If last char is decimal char (dot or comma) then this format has no decimal part:
                       
$format_suffix = '';
                       
$format_decimals = 0;
                       
$thousands_sep_pos = 3;
                    }
                    elseif(
in_array( $format[ $f_num - 3 ], array( '.', ',' ) ) )
                    {    
// Allow only chars '.' and ',' as decimal separator:
                       
if( $f_num > 3 && preg_match( '#^\d+$#', $format[ $f_num - 2 ] ) )
                        {    
// Get a number of digits after dot:
                           
$format_decimals = strlen( $format[ $f_num - 2 ] );
                        }
                        if(
$f_num > 4 && preg_match( '#^[^\d]+$#', $format[ $f_num - 3 ] ) )
                        {    
// Get a decimal point:
                           
$format_dec_point = $format[ $f_num - 3 ];
                        }
                       
$thousands_sep_pos = 5;
                    }
                    else
                    {    
// If format has no decimal part:
                       
$format_decimals = 0;
                       
$thousands_sep_pos = 3;
                    }
                    if(
$f_num > $thousands_sep_pos + 1 && preg_match( '#^[^\d]+$#', $format[ $f_num - $thousands_sep_pos ] ) )
                    {    
// Get a thousands separator:
                       
$format_thousands_sep = $format[ $f_num - $thousands_sep_pos ];
                    }
                   
// Format number with extracted data:
                   
$custom_field_value = number_format( floatval( $custom_field_value ), $format_decimals, $format_dec_point, $format_thousands_sep );
                }
               
// Add prefix and suffix:
               
$custom_field_value = $format_prefix.$custom_field_value.$format_suffix;
                if(
$format_class !== '' )
                {    
// Apply class for the format:
                   
$custom_field_value = '<span class="'.format_to_output( $format_class, 'htmlattr' ).'">'.$custom_field_value.'</span>';
                }
                break;

            case
'text':
               
// Escape html tags and convert new lines to html <br> for text fields:
               
$custom_field_value = nl2br( utf8_trim( utf8_strip_tags( $custom_field_value ) ) );
                break;

            case
'image':
               
// Display image fields as thumbnail:
               
$LinkCache = & get_LinkCache();
                if(
$Link = & $LinkCache->get_by_ID( $custom_field_value, false, false ) )
                {
                   
$custom_field_value = $Link->get_tag( array(
                       
'image_link_to' => false,
                       
'image_size'    => $format,
                    ) );
                }
                else
                {    
// Display an error if Link is not found in DB:
                   
$custom_field_value = get_rendering_error( T_('Invalid link ID:').' '.$custom_field_value, 'span' );
                }
                break;

            case
'url':
               
// Format URL field value:
               
if( $format === NULL || $format === '' )
                {    
// No format:
                   
break;
                }

               
$formats = explode( ';', $format );

                if(
$custom_field_value === '' || $custom_field_value === NULL )
                {    
// Use second format option for empty url:
                   
if( ! isset( $formats[1] ) || $formats[1] === '' )
                    {    
// No format for empty URL:
                       
return $custom_field_value;
                    }
                    else
                    {    
// Set a format for empty URL:
                       
$format_value = $formats[1];
                    }
                }
                else
                {    
// Use first format option for not empty url:
                   
$format_value = $formats[0];
                }

                if(
$format_value != '#url#' )
                {    
// Use specific text for link from format if it is not requested to use original url as link text:
                   
$custom_field_value = $format_value;
                }
                break;
        }

       
// Render special masks like #yes#, (+), #stars/3# and etc. in value with template:
       
$params['stars_value'] = $orig_custom_field_value;
       
$custom_field_value = render_custom_field( $custom_field_value, $params );

       
// Apply setting "Link to":
       
if( $custom_field['link'] != 'nolink' && ! empty( $custom_field_value ) )
        {
           
$link_fallbacks = array(
               
'linkpermzoom' => array( 'link', 'perm', 'zoom' ),
               
'permzoom'     => array( 'perm', 'zoom' ),
               
'linkperm'     => array( 'link', 'perm' ),
               
'linkto'       => array( 'link' ),
               
'permalink'    => array( 'perm' ),
               
'zoom'         => array( 'zoom' ),
               
'fieldurl'     => array( 'url' ),
               
'fieldurlblank'=> array( 'urlblank' ),
            );

            if( isset(
$link_fallbacks[ $custom_field['link'] ] ) )
            {
               
$fallback_count = count( $link_fallbacks[ $custom_field['link'] ] );
               
$link_class = trim( $custom_field['link_class'] );
               
$link_class_attr = ( $link_class === '' ? '' : ' class="'.format_to_output( $link_class, 'htmlattr' ).'"' );
               
$nofollow_attr = $custom_field['link_nofollow'] ? ' rel="nofollow"' : '';
                foreach(
$link_fallbacks[ $custom_field['link'] ] as $l => $link_fallback )
                {
                    switch(
$link_fallback )
                    {
                        case
'link':
                           
// Link to "URL":
                           
if( $this->get( 'url' ) != '' )
                            {    
// If this post has a specified setting "Link to url":
                               
$custom_field_value = '<a href="'.$this->get( 'url' ).'" target="_blank"'.$nofollow_attr.$link_class_attr.'>'.$custom_field_value.'</a>';
                                break
2;
                            }
                           
// else fallback to other points:
                           
break;

                        case
'perm':
                           
// Permalink:
                           
global $disp, $Item;
                            if( (
$disp != 'single' && $disp != 'page' ) ||
                                ! (
$Item instanceof Item ) ||
                               
$Item->ID != $this->ID ||
                               
$fallback_count == $l + 1 )
                            {    
// Use permalink if it is not last point and we don't view this current post:
                               
$custom_field_value = $this->get_permanent_link( $custom_field_value, '#', $link_class, '', '', NULL, array(), array( 'nofollow' => $custom_field['link_nofollow'] ) );
                                break
2;
                            }
                           
// else fallback to other points:
                           
break;

                        case
'zoom':
                           
// Link to zoom image:
                           
if( $custom_field['type'] == 'image' &&
                               
$LinkCache = & get_LinkCache() &&
                               
$Link = & $LinkCache->get_by_ID( $orig_custom_field_value, false, false ) &&
                               
$File = & $Link->get_File() )
                            {    
// Link to original file:
                               
$custom_field_value = '<a href="'.$File->get_url().'"'.( $File->is_image() ? ' rel="lightbox[p'.$this->ID.']"' : '' ).$link_class_attr.'>'.$custom_field_value.'</a>';
                            }
                           
// else fallback to other points:
                           
break;

                        case
'url':
                        case
'urlblank':
                           
// Use value of url fields as URL to the link:
                           
if( ! empty( $orig_custom_field_value ) )
                            {    
// Format URL to link only with not empty URL otherwise display URL as simple text if special text is defined in format for empty URL:
                               
$url_link_class = $custom_field['link_class'];
                                if(
$custom_field_value == $orig_custom_field_value )
                                {    
// Use word-break style only when original URL is used for link text because URL may contains very long single word:
                                   
$url_link_class .= ' linebreak';
                                }
                               
$custom_field_value = '<a href="'.$orig_custom_field_value.'"'
                                   
.$nofollow_attr
                                   
.' class="'.format_to_output( trim( $url_link_class ), 'htmlattr' ).'"'
                                   
.( $link_fallback == 'urlblank' ? ' target="_blank"' : '' ).'>'
                                       
.$custom_field_value
                                   
.'</a>';
                            }
                            break
2;
                    }
                }
            }
        }

        return
$custom_field_value;
    }


   
/**
     * Get computed item custom field value by field name
     *
     * @param string Field name, see {@link get_custom_fields_defs()}
     * @return string|boolean|NULL FALSE if the field doesn't exist, NULL if formula is invalid
     */
   
private function get_custom_field_computed( $field_name )
    {
       
// Get all custom fields by item ID:
       
$custom_fields = $this->get_custom_fields_defs();

        if( ! isset(
$custom_fields[ $field_name ] ) )
        {    
// The requested field is not detected:
           
return false;
        }

        if(
$custom_fields[ $field_name ]['type'] == 'double' )
        {    
// This case may be called by computing of the formula:
            // Use floatval() in order to consider empty value as 0
           
return floatval( $this->get_custom_field_value( $field_name ) );
        }

        if(
$custom_fields[ $field_name ]['type'] != 'computed' )
        {    
// The requested field is detected but it is not computed field:
           
return false;
        }

       
// Compute value by formula:
       
$formula = $custom_fields[ $field_name ]['formula'];
        if( empty(
$formula ) )
        {    
// Use NULL value because formula is empty:
           
return NULL;
        }

       
// Use NULL value for all cases below when formula is invalid or it cannot be computed by some unknown reason:
       
$custom_field_value = NULL;

        if( ! isset(
$this->cache_computed_custom_fields ) )
        {    
// Store in this array all computed fields to avoid recursion:
           
$this->cache_computed_custom_fields = array();
        }
        if(
in_array( $field_name, $this->cache_computed_custom_fields ) )
        {    
// Stop here because of recursion:
           
return NULL;
        }
       
$this->cache_computed_custom_fields[] = $field_name;

       
// Try to use a formula:
       
$formula_is_valid = true;
        if(
preg_match_all( '#\$([^$]+)\$#', $formula, $formula_match ) )
        {
            foreach(
$formula_match[1] as $formula_field_index )
            {
                if( ! isset(
$custom_fields[ $formula_field_index ] ) ||
                        !
in_array( $custom_fields[ $formula_field_index ]['type'], array( 'double', 'computed' ) ) ||
                        (
$formula_field_value = $this->get_custom_field_computed( $formula_field_index ) ) === false ||
                        !
is_numeric( $formula_field_value ) )
                {    
// Formula must use only custom fields with type "double"/"computed" and value must be a numeric:
                   
$formula_is_valid = false;
                   
// Stop here to don't check other fields because formula is already invalid:
                   
break;
                }
            }
        }

        if(
$formula_is_valid )
        {    
// Check functions in formula because all functions are forbidden in formula:
           
$formula_is_valid = ! preg_match( '#[a-z0-9_]+\s*\(.*?\)#i', $formula );
        }

        if(
$formula_is_valid )
        {    
// Try to compute a value if formula is valid:
           
$formula = preg_replace( '#\$([^$]+)\$#', '$this->get_custom_field_computed( \'$1\' )', $formula );
            try
            {    
// Compute value:
               
ob_start();
               
$custom_field_value = eval( "return $formula;" );
               
$formula_code_output = ob_get_clean();
                if( (
$formula_code_output !== '' && $formula_code_output !== false ) ||
                        !
is_numeric( $custom_field_value ) )
                {    
// If output buffer contains some text it means there is some error;
                    // Don't allow to use not numeric value for the "computed" custom field:
                   
$custom_field_value = NULL;
                }
            }
            catch(
Error $e )
            {    
// Set NULL value for wrong formula:
               
$custom_field_value = NULL;
            }
            catch(
ParseError $e )
            {    
// Set NULL value for wrong formula:
               
$custom_field_value = NULL;
            }
        }

       
// Unset temp array at the end of recursion:
       
unset( $this->cache_computed_custom_fields );

        return
$custom_field_value;
    }


   
/**
     * TEMPLATE TAG: Display custom field
     *
     * @param array Params
     */
   
public function custom( $params )
    {
       
// Make sure we are not missing any param:
       
$params = array_merge( array(
               
// required: 'field'
               
'what'   => 'formatted_value', // 'label' - to display label of the custom field
                 
'before' => ' ',
               
'after'  => ' ',
            ),
$params );

        if( empty(
$params['field'] ) )
        {
            return;
        }

       
// Load custom field by index:
       
$custom_fields = $this->get_custom_fields_defs();
       
$field_name = $params['field'];
        if( ! isset(
$custom_fields[ $field_name ] ) )
        {
// Custom field with this index doesn't exist
           
display_rendering_error( sprintf( T_('The custom field %s does not exist!'), '<b>'.$field_name.'</b>' ), 'span' );
            return;
        }

        switch(
$params['what'] )
        {
            case
'label':
               
$r = $this->get_custom_field_title( $params['field'] );
                break;

            default:
// formatted_value
               
$r = $this->get_custom_field_formatted( $field_name, $params );
                break;
        }

        if(
is_string( $r ) && $r !== '' )
        {    
// Print if value is not empty string:
           
echo $params['before'].$r.$params['after'];
        }
    }


   
/**
     * Display all custom fields of current Item
     *
     * @param array Params
     */
   
public function custom_fields( $params = array() )
    {
        echo
$this->get_custom_fields( $params );
    }


   
/**
     * Get all custom fields of this Item as HTML code
     *
     * @param array Params
     * @return string
     */
   
public function get_custom_fields( $params = array() )
    {
       
// Make sure we are not missing any param:
       
$params = array_merge( array(
               
'fields'        => '', // Empty string to display ALL fields, OR fields names separated by comma to show/hide only the requested fields in order what you want
               
'fields_source' => 'include', // 'all' - All item's fields, 'exclude' - All except fields listed in the param 'fields', 'include' - Only fields listed in the param 'fields'
                // See default template params in the widget function item_fields_compare_Widget->display():
           
), $params );

       
// Convert fields separator to widget format:
       
$custom_fields = str_replace( ',', "\n", trim( $params['fields'], ', ' ) );

       
// Call widget with params only when content is not generated yet above:
       
ob_start();
       
skin_widget( array_merge( $params, array(
               
'widget'        => 'item_custom_fields',
               
'fields_source' => empty( $custom_fields ) ? 'all' : $params['fields_source'],
               
'fields'        => $custom_fields,
               
'items'         => $this->ID,
            ) ) );
       
$widget_html = ob_get_contents();
       
ob_end_clean();

        return
$widget_html;
    }


   
/**
     * Convert all inline tags to HTML code
     *
     * @param string Source content
     * @param array Params
     * @return string Content
     */
   
function render_inline_tags( $content, $params = array() )
    {
       
$params = array_merge( array(
               
'check_code_block'      => true, // TRUE to find inline tags only outside of codeblocks
                // render_content_blocks():
               
'render_tag_include'           => true,
               
'render_tag_cblock'            => true,
               
// render_inline_widgets():
               
'render_tag_item_subscribe'    => true,
               
'render_tag_item_emailcapture' => true,
               
'render_tag_item_compare'      => true,
               
'render_tag_item_fields'       => true,
               
// render_block_widgets():
               
'render_tag_switcher'          => true,
               
// render_inline_files():
               
'render_tag_image'             => true,
               
'render_tag_file'              => true,
               
'render_tag_inline'            => true,
               
'render_tag_video'             => true,
               
'render_tag_audio'             => true,
               
'render_tag_thumbnail'         => true,
               
'render_tag_folder'            => true,
               
// render_link_data():
               
'render_tag_item_link'         => true,
               
// render_custom_fields():
               
'render_tag_field'             => true,
               
// render_other_item_data():
               
'render_tag_item_field'        => true,
               
'render_tag_item_titlelink'    => true,
               
'render_tag_item_url'          => true,
               
// render_collection_data():
               
'render_tag_coll_name'         => true,
               
'render_tag_coll_shortname'    => true,
               
// render_switchable_blocks():
               
'render_tag_switchable_div'    => true,
               
// render_templates():
               
'render_tag_template'          => true,
            ),
$params );

       
// Remove block level short tags inside <p> blocks and move them before the paragraph:
       
$content = move_short_tags( $content );

       
// Render Content block tags like [include:123], [include:item-slug], [cblock:123], [cblock:item-slug]:
       
$content = $this->render_content_blocks( $content, $params );

       
// Render widget tags (subscribe, emailcapture, compare, fields):
       
$content = $this->render_inline_widgets( $content, $params );

       
// Render widget tags (switcher):
       
$content = $this->render_block_widgets( $content, $params );

       
// Render inline file tags like [image:123:caption] or [file:123:caption]:
       
$content = render_inline_files( $content, $this, array_merge( $params, array(
               
'clear_paragraph' => false, // Don't clear paragraph twice
           
) ) );

       
// Render Collection Data [link:url_field], [link:url_field]title[/link] and etc.:
       
$content = $this->render_link_data( $content, $params );

       
// Render single value of Custom Fields [field:first_string_field]:
       
$content = $this->render_custom_fields( $content, $params );

       
// Render parent/other item data [parent:titlelink], [parent:url], [parent:field:first_string_field], [item:123:titlelink], [item:slug:titlelink] and etc.:
       
$content = $this->render_other_item_data( $content, $params );

       
// Render Collection Data [coll:name], [coll:shortname]:
       
$content = $this->render_collection_data( $content, $params );

       
// Render switchable block tags like [div::view=detailed]Multiline Content Text[/div]:
       
$content = $this->render_switchable_blocks( $content, $params );

       
// Render template tags like [template:template_code|param1=value1|param2=value2]:
       
$content = $this->render_templates( $content, $params );

        if( !
check_user_perm( 'item_post!CURSTATUS', 'edit', false, $this ) )
        {    
// Clean up rendering errors from content if current User has no permission to edit this Item:
           
$content = clear_rendering_errors( $content );
        }

        return
$content;
    }


   
/**
     * Convert inline widget tags like [subscribe], [emailcapture], [compare], [fields], [parent:fields], [item:123:fields], [item:slug:fields] into HTML tags
     *
     * @param string Source content
     * @param array Params
     * @return string Content
     */
   
function render_inline_widgets( $content, $params )
    {
        global
$Settings;

       
$params = array_merge( array(
               
'render_tag_item_subscribe'    => true,
               
'render_tag_item_emailcapture' => true,
               
'render_tag_item_compare'      => true,
               
'render_tag_item_fields'       => true,
            ),
$params );

       
$render_tags = array();
        if(
$params['render_tag_item_subscribe'] )
        {    
// Render short tag [subscribe:]
           
$render_tags[] = 'subscribe';
        }
        if(
$params['render_tag_item_emailcapture'] )
        {    
// Render short tag [emailcapture:]
           
$render_tags[] = 'emailcapture';
        }
        if(
$params['render_tag_item_compare'] )
        {    
// Render short tag [compare:]
           
$render_tags[] = 'compare';
        }
        if(
$params['render_tag_item_fields'] )
        {    
// Render short tag [fields:]
           
$render_tags[] = 'fields';
        }

        if( empty(
$render_tags ) )
        {    
// No tags for rendering:
           
return $content;
        }

       
load_funcs( 'skins/_skin.funcs.php' );
        if( isset(
$params['check_code_block'] ) && $params['check_code_block'] && ( ( stristr( $content, '<code' ) !== false ) || ( stristr( $content, '<pre' ) !== false ) ) )
        {    
// Call $this->render_collection_data() on everything outside code/pre:
           
$params['check_code_block'] = false;
           
$content = callback_on_non_matching_blocks( $content,
               
'~<(code|pre)[^>]*>.*?</\1>~is',
                array(
$this, 'render_inline_widgets' ), array( $params ) );
            return
$content;
        }

       
// Find all matches with tags of widgets:
       
preg_match_all( '/\[(parent:|item:[^:\]]+:)?('.implode( '|', $render_tags ).'):?([^\]]*)\]/i', $content, $tags );

        if(
count( $tags[0] ) > 0 )
        {    
// If at least one widget tag is found in content:
           
foreach( $tags[0] as $t => $source_tag )
            {    
// Render URL custom field as html:
               
$field_Item = $this;
               
$widget_params = false;
               
$widget_html = false;
               
$tag_prefix = $tags[1][$t];
               
$widget_name = $tags[2][$t];
               
$tag_params = explode( ':', $tags[3][$t] );
                switch(
$widget_name )
                {
                    case
'subscribe':
                       
// Widget "Newsletter/Email list subscription":
                       
$button_notsubscribed = '';
                       
$button_subscribed = '';
                       
$button_notloggedin = '';

                       
preg_match( '/(\d+)(?:\/(.*))?/', $tag_params[0], $newsletter_ID_tags );
                       
$newsletter_ID = intval( $newsletter_ID_tags[1] );
                        if( isset(
$newsletter_ID_tags[2] ) )
                        {
                           
$user_tags = $newsletter_ID_tags[2];
                        }

                        if( isset(
$tag_params[1] ) )
                        {
                           
$button_notsubscribed = $tag_params[1];
                        }

                        if( isset(
$tag_params[2] ) )
                        {
                           
$button_subscribed = $tag_params[2];
                        }

                        if( isset(
$tag_params[3] ) )
                        {
                           
$button_notloggedin = $tag_params[3];
                        }

                       
$widget_params = array(
                           
'widget' => 'newsletter_subscription',
                           
'title' => '',
                           
'intro' => '',
                           
'bottom' => '',
                           
'title_subscribed' => '',
                           
'intro_subscribed' => '',
                           
'bottom_subscribed' => '',
                           
'enlt_ID' => $newsletter_ID,
                           
'button_notsubscribed_class' => 'btn-danger',
                           
'button_subscribed_class' => 'btn-success',
                           
'inline' => 1
                       
);
                        if( ! empty(
$button_notsubscribed ) )
                        {
                           
$widget_params['button_notsubscribed'] = $button_notsubscribed;
                        }
                        if( ! empty(
$button_subscribed ) )
                        {
                           
$widget_params['button_subscribed'] = $button_subscribed;
                        }
                        if( ! empty(
$user_tags ) )
                        {
                           
$widget_params['usertags'] = $user_tags;
                           
$widget_params['unsubscribed_if_not_tagged'] = true;
                        }

                        if( ! empty(
$button_notloggedin ) && ! is_logged_in() )
                        {
// Email capture widget does not display if user is not logged in
                           
$redirect_to = regenerate_url( '', '', '', '&' );
                           
$widget_html = '<div class="center">';
                           
$widget_html .= '<a href="'.get_login_url( 'inline subscribe', $redirect_to ).'" class="btn btn-primary">'.$button_notloggedin.'</a>';
                           
$widget_html .= '</div>';
                        }
                        break;

                    case
'emailcapture':
                       
// Widget "Email capture / Quick registration":
                       
preg_match( '/(\d+)?(?:\/(.*))?/', $tag_params[0], $newsletter_ID_tags );
                       
$newsletter_ID = isset( $newsletter_ID_tags[1] ) ? intval( $newsletter_ID_tags[1] ) : NULL;
                       
$user_tags = isset( $newsletter_ID_tags[2] ) ? $newsletter_ID_tags[2] : NULL;
                       
$fields_to_display = isset( $tag_params[1] ) ? explode( '+', $tag_params[1] ) : array();
                       
$button_text = isset( $tag_params[2] ) ? $tag_params[2] : '';

                       
$widget_params = array(
                           
'widget' => 'user_register_quick',
                           
'title' => '',
                           
'intro' => '',
                           
'ask_firstname' => in_array( 'firstname', $fields_to_display ) ? 'required' : 'no',
                           
'ask_lastname' => in_array( 'lastname', $fields_to_display ) ? 'required' : 'no',
                           
'ask_country' => in_array( 'country', $fields_to_display ) ? 'required' : 'no',
                           
'source' => 'Page: '.$this->get( 'urltitle' ),
                           
'usertags' => $user_tags,
                           
'subscribe' => array( 'post' => 0, 'comment' => 0 ),
                           
'button_class' => 'btn-primary',
                           
'inline' => 1
                       
);

                       
$NewsletterCache = & get_NewsletterCache();
                       
$load_where = 'enlt_active = 1';
                       
$NewsletterCache->load_where( $load_where );
                       
// Initialize checkbox options for param "Newsletter":
                       
$newsletters_options = array();
                       
$def_newsletters = explode( ',', $Settings->get( 'def_newsletters' ) );
                        foreach(
$NewsletterCache->cache as $Newsletter )
                        {
                           
$newsletters_options[] = array(
                               
$Newsletter->ID,
                               
$Newsletter->get( 'name' ).': '.$Newsletter->get( 'label' ),
                               
$Newsletter->ID == $newsletter_ID ? 1 : 0, // checked if specified newsletter ID
                           
);
                        }
                       
$newsletters_options[] = array(
                           
'default',
                           
T_('Also subscribe user to all default newsletters for new users.'),
                            empty(
$newsletter_ID ) ? 1 : 0, // checked if no specific newsletter ID specified
                       
);
                       
$widget_params['newsletters'] = $newsletters_options;

                        if( ! empty (
$button_text ) )
                        {
                           
$widget_params['button'] = $button_text;
                        }
                        break;

                    case
'compare':
                       
// Widget "Compare Item Fields":
                        // Set item IDs to compare:
                       
$compare_items = isset( $tag_params[0] ) ? trim( $tag_params[0], ', ' ) : '';
                        if( empty(
$compare_items ) )
                        {    
// Skip a compare tag without item IDs:
                           
break;
                        }
                       
// Set fields to compare:
                       
$compare_fields = isset( $tag_params[1] ) ? str_replace( ',', "\n", trim( $tag_params[1], ', ' ) ) : '';
                       
// Set widget params to display:
                       
$widget_params = array(
                           
'widget'        => 'item_fields_compare',
                           
'items_source'  => 'list',
                           
'items'         => $compare_items,
                           
'fields_source' => empty( $compare_fields ) ? 'all' : 'include',
                           
'fields'        => $compare_fields,
                        );
                        break;

                    case
'fields':
                       
// Widget "Item Custom Fields":
                       
if( $tag_prefix == 'parent:' )
                        {    
// Use parent item:
                           
if( ! ( $widget_Item = & $this->get_parent_Item() ) )
                            {    
// Display error message if parent doesn't exist:
                               
$widget_html = get_rendering_error( T_('This Item has no parent.'), 'span' );
                                break;
                            }
                           
$widget_item_ID = $widget_Item->ID;
                        }
                        elseif(
strpos( $tag_prefix, 'item:' ) === 0 )
                        {    
// Use other item by ID or slug:
                           
$widget_item_ID_slug = substr( $tag_prefix, 5, -1 );
                           
$widget_item_data_is_number = is_number( $widget_item_ID_slug );
                           
$ItemCache = & get_ItemCache();
                            if( ! (
$widget_item_data_is_number && $widget_Item = & $ItemCache->get_by_ID( $widget_item_ID_slug, false, false ) ) &&
                                    ! ( !
$widget_item_data_is_number && $widget_Item = & $ItemCache->get_by_urltitle( $widget_item_ID_slug, false, false ) ) )
                            {    
// Display error message if other item is not found by ID and slug:
                               
$widget_html = get_rendering_error( sprintf( T_('The Item %s doesn\'t exist.'), '<code>'.$widget_item_ID_slug.'</code>' ), 'span' );
                                break;
                            }
                           
$widget_item_ID = $widget_Item->ID;
                        }
                        else
                        {    
// Use current Item:
                           
$widget_item_ID = $this->ID;
                           
$widget_Item = $this;
                        }

                       
$custom_fields = $widget_Item->get_custom_fields_defs();
                        if( !
$custom_fields )
                        {    
// Fields don't exist for this Item:
                           
$widget_html = get_rendering_error( T_('The Item has no custom fields.'), 'span' );
                            break;
                        }

                       
// Set fields to display:
                       
$custom_fields = isset( $tag_params[0] ) ? str_replace( ',', "\n", trim( $tag_params[0], ', ' ) ) : '';
                       
// Set widget params to display:
                       
$widget_params = array(
                           
'widget'        => 'item_custom_fields',
                           
'fields_source' => empty( $custom_fields ) ? 'all' : 'include',
                           
'fields'        => $custom_fields,
                           
'items'         => $widget_item_ID,
                        );
                        break;
                }

               
// If widget display params are initialized for the inline tag:
               
if( $widget_params !== false && $widget_html === false )
                {    
// Call widget with params only when content is not generated yet above:
                   
ob_start();
                   
skin_widget( array_merge( $params, $widget_params ) );
                   
$widget_html = ob_get_contents();
                   
ob_end_clean();
                }
                if(
$widget_html !== false )
                {    
// Replace inline widget tag with content generated by requested widget:
                   
$content = substr_replace( $content, $widget_html, strpos( $content, $source_tag ), strlen( $source_tag ) );
                }
            }
        }

        return
$content;
    }


   
/**
     * Convert block widget tags like [switcher:param_name][option:value]Text[/option][/switcher] into HTML tags
     *
     * @param string Source content
     * @param array Params
     * @return string Content
     */
   
function render_block_widgets( $content, $params )
    {
        global
$Settings;

       
$params = array_merge( array(
               
'render_tag_switcher' => true,
            ),
$params );

        if( !
$params['render_tag_switcher'] )
        {    
// No tags for rendering:
           
return $content;
        }

       
load_funcs( 'skins/_skin.funcs.php' );
        if( isset(
$params['check_code_block'] ) && $params['check_code_block'] && ( ( stristr( $content, '<code' ) !== false ) || ( stristr( $content, '<pre' ) !== false ) ) )
        {    
// Call $this->render_collection_data() on everything outside code/pre:
           
$params['check_code_block'] = false;
           
$content = callback_on_non_matching_blocks( $content,
               
'~<(code|pre)[^>]*>.*?</\1>~is',
                array(
$this, 'render_block_widgets' ), array( $params ) );
            return
$content;
        }

       
// Find all matches with tags of widgets:
       
if( ! preg_match_all( '#\[(switcher)(:.+?)?\](.*?)\[/\1\]#is', $content, $tags ) )
        {    
// No found tags:
           
return $content;
        }

        foreach(
$tags[0] as $t => $source_tag )
        {
           
$widget_params = false;
           
$widget_html = false;
           
$widget_name = $tags[1][$t];
           
$tag_params = explode( ':', trim( $tags[2][$t], ':' ) );
            switch(
$widget_name )
            {
                case
'switcher':
                   
// Widget "Param Switcher":s
                   
if( ! isset( $tag_params[0] ) || $tag_params[0] === '' )
                    {    
// Skip wrong configured tag:
                       
$widget_html = get_rendering_error( T_('Param code must be defined for switcher tag!'), 'span' );
                        break;
                    }

                   
$widget_buttons = array();
                    if(
preg_match_all( '#\[(option):(.+?)\](.+?)\[/\1\]#is', $tags[3][$t], $tag_options ) )
                    {    
// Initialize buttons for widget "Param Switcher":
                       
foreach( $tag_options[2] as $o => $tag_option_value )
                        {
                           
$widget_buttons[] = array(
                               
'value' => $tag_option_value,
                               
'text'  => $tag_options[3][$o],
                            );
                        }
                    }
                    if( empty(
$widget_buttons ) )
                    {    
// Don't try to render widget without buttons:
                       
$widget_html = get_rendering_error( T_('At least one button must be defined for switcher tag!'), 'span' );
                        break;
                    }

                   
// Set widget params to display:
                   
$widget_params = array(
                       
'widget'     => 'param_switcher',
                       
'param_code' => $tag_params[0],
                       
'buttons'    => $widget_buttons,
                    );

                    if( isset(
$tag_params[1] ) && in_array( $tag_params[1], array( 'auto', 'list', 'buttons' ) ) )
                    {    
// Set a display mode:
                       
$widget_params['display_mode'] = $tag_params[1];
                    }
                    break;
            }

           
// If widget display params are initialized for the inline tag:
           
if( $widget_params !== false && $widget_html === false )
            {    
// Call widget with params only when content is not generated yet above:
               
ob_start();
               
skin_widget( array_merge( $params, $widget_params ) );
               
$widget_html = ob_get_contents();
               
ob_end_clean();
            }
            if(
$widget_html !== false )
            {    
// Replace inline widget tag with content generated by requested widget:
               
$content = substr_replace( $content, $widget_html, strpos( $content, $source_tag ), strlen( $source_tag ) );
            }
        }

        return
$content;
    }


   
/**
     * Convert inline custom field tags like [field:first_string_field] into HTML tags
     *
     * @param string Source content
     * @param array Params
     * @return string Content
     */
   
function render_custom_fields( $content, $params = array() )
    {
       
$params = array_merge( array(
               
'render_tag_field' => true,
            ),
$params );

        if( !
$params['render_tag_field'] )
        {    
// No tags for rendering:
           
return $content;
        }

        if( isset(
$params['check_code_block'] ) && $params['check_code_block'] && ( ( stristr( $content, '<code' ) !== false ) || ( stristr( $content, '<pre' ) !== false ) ) )
        {    
// Call $this->render_custom_fields() on everything outside code/pre:
           
$params['check_code_block'] = false;
           
$content = callback_on_non_matching_blocks( $content,
               
'~<(code|pre)[^>]*>.*?</\1>~is',
                array(
$this, 'render_custom_fields' ), array( $params ) );
            return
$content;
        }

       
// Find all matches with tags of custom fields:
       
preg_match_all( '/\[field:([^\]]*)?\]/i', $content, $tags );

        foreach(
$tags[0] as $t => $source_tag )
        {
           
// Render single field as text:
           
$field_name = trim( $tags[1][ $t ] );
           
$field_value = $this->get_custom_field_formatted( $field_name, $params );
            if(
$field_value === false )
            {    
// Wrong field request, display error:
               
$content = str_replace( $source_tag, get_rendering_error( sprintf( T_('The field "%s" does not exist.'), $field_name ), 'span' ), $content );
            }
            else
            {    
// Display field value:
               
$custom_fields = $this->get_custom_fields_defs();
                if(
$custom_fields[ $field_name ]['public'] )
                {    
// Display value only if custom field is public:
                   
$content = str_replace( $source_tag, $field_value, $content );
                }
                else
                {    
// Display an error for not public custom field:
                   
$content = str_replace( $source_tag, get_rendering_error( sprintf( T_('The field "%s" is not public.'), $field_name ), 'span' ), $content );
                }
            }
        }

        return
$content;
    }


   
/**
     * Convert inline parent/other item tags into HTML tags like:
     *    [parent:titlelink]
     *    [parent:url]
     *    [parent:field:first_string_field]
     *    [item:123:titlelink]
     *    [item:123:url]
     *    [item:123:field:first_string_field]
     *    [item:slug:titlelink]
     *    [item:slug:url]
     *    [item:slug:field:first_string_field]
     *
     * @param string Source content
     * @param array Params
     * @return string Content
     */
   
function render_other_item_data( $content, $params = array() )
    {
       
$params = array_merge( array(
               
'render_tag_item_field'     => true,
               
'render_tag_item_titlelink' => true,
               
'render_tag_item_url'       => true,
            ),
$params );

       
$render_tags = array();
        if(
$params['render_tag_item_field'] )
        {    
// Render short tag [item:123:field:]
           
$render_tags[] = 'field';
        }
        if(
$params['render_tag_item_titlelink'] )
        {    
// Render short tag [item:123:titlelink]
           
$render_tags[] = 'titlelink';
        }
        if(
$params['render_tag_item_url'] )
        {    
// Render short tag [item:123:url]
           
$render_tags[] = 'url';
        }

        if( empty(
$render_tags ) )
        {    
// No tags for rendering:
           
return $content;
        }

        if( isset(
$params['check_code_block'] ) && $params['check_code_block'] && ( ( stristr( $content, '<code' ) !== false ) || ( stristr( $content, '<pre' ) !== false ) ) )
        {    
// Call $this->render_other_item_data() on everything outside code/pre:
           
$params['check_code_block'] = false;
           
$content = callback_on_non_matching_blocks( $content,
               
'~<(code|pre)[^>]*>.*?</\1>~is',
                array(
$this, 'render_other_item_data' ), array( $params ) );
            return
$content;
        }

       
// Find all matches with tags of parent data:
       
preg_match_all( '/\[(parent|item:[^:]+):('.implode( '|', $render_tags ).'):?([^\]]*)?\]/i', $content, $tags );

        if(
count( $tags[0] ) > 0 )
        {    
// If at least one other item tag is found in content:
           
foreach( $tags[0] as $t => $source_tag )
            {
                if(
$tags[1][ $t ] == 'parent' )
                {    
// Get data of item parent:
                   
if( ! ( $other_Item = & $this->get_parent_Item() ) )
                    {    
// Display error message if parent doesn't exist:
                       
$content = str_replace( $tags[0][ $t ], get_rendering_error( T_('This Item has no parent.'), 'span' ), $content );
                        continue;
                    }
                }
                else
                {    
// Try to use other item by ID or slug:
                   
$other_item_ID_slug = substr( $tags[1][ $t ], 5 );
                   
$other_item_data_is_number = is_number( $other_item_ID_slug );
                   
$ItemCache = & get_ItemCache();
                    if( ! (
$other_item_data_is_number && $other_Item = & $ItemCache->get_by_ID( $other_item_ID_slug, false, false ) ) &&
                        ! ( !
$other_item_data_is_number && $other_Item = & $ItemCache->get_by_urltitle( $other_item_ID_slug, false, false ) ) )
                    {    
// Display error message if other item is not found by ID and slug:
                       
$content = str_replace( $tags[0][ $t ], get_rendering_error( sprintf( T_('The Item %s doesn\'t exist.'), '<code>'.$other_item_ID_slug.'</code>' ), 'span' ), $content );
                        continue;
                    }
                }

                switch(
$tags[2][ $t ] )
                {
                    case
'field':
                       
// Render single parent custom field as text:
                       
$field_name = trim( $tags[3][ $t ] );
                       
$field_value = $other_Item->get_custom_field_formatted( $field_name, $params );
                        if(
$field_value === false )
                        {    
// Wrong field request, display error:
                           
$content = str_replace( $source_tag, get_rendering_error( sprintf( T_('The field "%s" does not exist.'), $field_name ), 'span' ), $content );
                        }
                        else
                        {    
// Display field value:
                           
$custom_fields = $other_Item->get_custom_fields_defs();
                            if(
$custom_fields[ $field_name ]['public'] )
                            {    
// Display value only if custom field is public:
                               
$content = str_replace( $source_tag, $field_value, $content );
                            }
                            else
                            {    
// Display an error for not public custom field:
                               
$content = str_replace( $source_tag, get_rendering_error( sprintf( T_('The field "%s" is not public.'), $field_name ), 'span' ), $content );
                            }
                        }
                        break;

                    case
'titlelink':
                       
// Render parent title with link:
                       
$content = str_replace( $source_tag, $other_Item->get_title(), $content );
                        break;

                    case
'url':
                       
// Render parent URL:
                       
$content = str_replace( $source_tag, $other_Item->get_permanent_url(), $content );
                        break;
                }
            }
        }

        return
$content;
    }


   
/**
     * Convert inline collection tags into HTML tags like:
     *    [coll:name]
     *    [coll:shortname]
     *
     * @param string Source content
     * @param array Params
     * @return string Content
     */
   
function render_collection_data( $content, $params = array() )
    {
       
$params = array_merge( array(
               
'render_tag_coll_name'      => true,
               
'render_tag_coll_shortname' => true,
            ),
$params );

       
$render_tags = array();
        if(
$params['render_tag_coll_name'] )
        {    
// Render short tag [coll:name]
           
$render_tags[] = 'name';
        }
        if(
$params['render_tag_coll_shortname'] )
        {    
// Render short tag [coll:shortname]
           
$render_tags[] = 'shortname';
        }

        if( empty(
$render_tags ) )
        {    
// No tags for rendering:
           
return $content;
        }

        if( isset(
$params['check_code_block'] ) && $params['check_code_block'] && ( ( stristr( $content, '<code' ) !== false ) || ( stristr( $content, '<pre' ) !== false ) ) )
        {    
// Call $this->render_collection_data() on everything outside code/pre:
           
$params['check_code_block'] = false;
           
$content = callback_on_non_matching_blocks( $content,
               
'~<(code|pre)[^>]*>.*?</\1>~is',
                array(
$this, 'render_collection_data' ), array( $params ) );
            return
$content;
        }

       
// Find all matches with tags of collection data:
       
preg_match_all( '/\[coll:('.implode( '|', $render_tags ).')\]/i', $content, $tags );

        if(
count( $tags[0] ) > 0 )
        {    
// If at least one collection tag is found in content:
           
$item_Blog = & $this->get_Blog();

            foreach(
$tags[0] as $t => $source_tag )
            {
                switch(
$tags[1][ $t ] )
                {
                    case
'name':
                       
// Render collection name:
                       
$content = str_replace( $source_tag, $item_Blog->get( 'name' ), $content );
                        break;

                    case
'shortname':
                       
// Render collection short name:
                       
$content = str_replace( $source_tag, $item_Blog->get( 'shortname' ), $content );
                        break;
                }
            }
        }

        return
$content;
    }


   
/**
     * Convert inline link tags into HTML tags like:
     *    [link:url_field]
     *    [link:url_field]title[/link]
     *    [link:url_field:.class1.class2]title[/link]
     * url_field is code of custom item field with type "URL"
     *
     * @param string Source content
     * @param array Params
     * @return string Content
     */
   
function render_link_data( $content, $params = array() )
    {
       
$params = array_merge( array(
               
'render_tag_item_link' => true,
            ),
$params );

        if( !
$params['render_tag_item_link'] )
        {    
// No tags for rendering:
           
return $content;
        }

        if( isset(
$params['check_code_block'] ) && $params['check_code_block'] && ( ( stristr( $content, '<code' ) !== false ) || ( stristr( $content, '<pre' ) !== false ) ) )
        {    
// Call $this->render_link_data() on everything outside code/pre:
           
$params['check_code_block'] = false;
           
$content = callback_on_non_matching_blocks( $content,
               
'~<(code|pre)[^>]*>.*?</\1>~is',
                array(
$this, 'render_link_data' ), array( $params ) );
            return
$content;
        }

       
// Find all matches with tags of link data:
       
preg_match_all( '/\[(parent:|item:[^:]+:)?link:([^\]]+)\]((.*?)\[\/link\])?/i', $content, $tags );

        if(
count( $tags[0] ) > 0 )
        {    
// If at least one link tag is found in content:
           
foreach( $tags[0] as $t => $source_tag )
            {    
// Render URL custom field as html:
               
if( $tags[1][ $t ] == 'parent:' )
                {    
// Try to use parent:
                   
if( ! ( $other_Item = & $this->get_parent_Item() ) )
                    {    
// Display error message if parent doesn't exist:
                       
$content = substr_replace( $content, get_rendering_error( T_('This Item has no parent.'), 'span' ), strpos( $content, $source_tag ), strlen( $source_tag ) );
                        continue;
                    }
                }
                elseif( ! empty(
$tags[1][ $t ] ) )
                {    
// Try to use other item by ID or slug:
                   
$other_item_ID_slug = rtrim( substr( $tags[1][ $t ], 5 ), ':' );
                   
$other_item_data_is_number = is_number( $other_item_ID_slug );
                   
$ItemCache = & get_ItemCache();
                    if( ! (
$other_item_data_is_number && $other_Item = & $ItemCache->get_by_ID( $other_item_ID_slug, false, false ) ) &&
                        ! ( !
$other_item_data_is_number && $other_Item = & $ItemCache->get_by_urltitle( $other_item_ID_slug, false, false ) ) )
                    {    
// Display error message if other item is not found by ID and slug:
                       
$content = str_replace( $tags[0][ $t ], get_rendering_error( sprintf( T_('The Item %s doesn\'t exist.'), '<code>'.$other_item_ID_slug.'</code>' ), 'span' ), $content );
                        continue;
                    }
                }
                else
                {    
// Use this Item:
                   
$other_Item = $this;
                }

               
$link_data = explode( ':', $tags[2][ $t ] );

               
// Get field code:
               
$url_field_code = trim( $link_data[0] );

               
$custom_fields = $other_Item->get_custom_fields_defs();
               
$field_value = $other_Item->get_custom_field_value( $url_field_code, 'url' );
                if(
$field_value === false )
                {    
// Wrong field request, display error:
                   
$link_html = get_rendering_error( sprintf( T_('The field "%s" does not exist.'), $url_field_code ), 'span' );
                }
                elseif( !
$custom_fields[ $url_field_code ]['public'] )
                {    
// Display an error for not public custom field:
                   
$link_html = get_rendering_error( sprintf( T_('The field "%s" is not public.'), $url_field_code ), 'span' );
                }
                elseif(
$field_value === '' )
                {    
// Empty field value, display error:
                   
$link_html = get_rendering_error( sprintf( T_('Referenced URL field is empty.'), $url_field_code ), 'span' );
                }
                else
                {    
// Display URL field as html link:
                   
$link_class = empty( $link_data[1] ) ? '' : str_replace( '.', ' ', $link_data[1] );
                    if( empty(
$tags[4][ $t ] ) )
                    {    
// Add style class to break long urls:
                       
$link_class .= ' linebreak';
                    }
                   
$link_class = ' class="'.trim( $link_class ).'"';
                   
$link_text = empty( $tags[4][ $t ] ) ? $field_value : $tags[4][ $t ];
                   
$link_html = '<a href="'.$field_value.'"'.$link_class.'>'.$link_text.'</a>';
                }
               
$content = substr_replace( $content, $link_html, strpos( $content, $source_tag ), strlen( $source_tag ) );
            }
        }

        return
$content;
    }


   
/**
     * Convert inline content block tags like [include:123], [include:item-slug], [cblock:123], [cblock:item-slug] into item/post content
     *
     * @param string Source content
     * @param array Params
     * @return string Content
     */
   
function render_content_blocks( $content, $params = array() )
    {
        global
$content_block_items;

       
$params = array_merge( array(
               
'render_tag_include' => true,
               
'render_tag_cblock'  => true,
            ),
$params );

       
$render_tags = array();
        if(
$params['render_tag_include'] )
        {    
// Render short tag [include:]
           
$render_tags[] = 'include';
        }
        if(
$params['render_tag_cblock'] )
        {    
// Render short tag [cblock:]
           
$render_tags[] = 'cblock';
        }

        if( empty(
$render_tags ) )
        {    
// No tags for rendering:
           
return $content;
        }

        if( isset(
$params['check_code_block'] ) && $params['check_code_block'] && ( ( stristr( $content, '<code' ) !== false ) || ( stristr( $content, '<pre' ) !== false ) ) )
        {    
// Call $this->render_content_blocks() on everything outside code/pre:
           
$params['check_code_block'] = false;
           
$content = callback_on_non_matching_blocks( $content,
               
'~<(code|pre)[^>]*>.*?</\1>~is',
                array(
$this, 'render_content_blocks' ), array( $params ) );
            return
$content;
        }

       
// Find all matches with tags of content block posts:
       
preg_match_all( '/\[('.implode( '|', $render_tags ).'):?([^\]]*)?\]/i', $content, $tags );

       
$ItemCache = & get_ItemCache();

       
$item_Blog = & $this->get_Blog();

        foreach(
$tags[0] as $t => $source_tag )
        {
           
$tag_options = explode( ':', $tags[2][ $t ] );

           
$item_ID_slug = trim( $tag_options[0] );

            if(
$item_ID_slug === '' )
            {    
// Don't render inline content block tag without specified item:
               
continue;
            }

            if( ! (
$content_Item = & $ItemCache->get_by_ID( $item_ID_slug, false, false ) ) )
            {    
// Try to get item by slug if it is not found by ID:
               
$content_Item = & $ItemCache->get_by_urltitle( $item_ID_slug, false, false );
            }

            if( !
$content_Item || $content_Item->get_type_setting( 'usage' ) != 'content-block' )
            {    
// Item is not found by ID and slug or it is not a content block:
               
if( $content_Item )
                {    
// It is not a content block:
                   
$wrong_item_info = '#'.$content_Item->ID.' '.$content_Item->get( 'title' );
                }
                else
                {    
// Item is not found:
                   
$wrong_item_info = '<code>'.$item_ID_slug.'</code>';
                }
               
// Replace inline content block tag with error message about wrong referenced item:
               
$content = str_replace( $source_tag, get_rendering_error( sprintf( T_('The referenced Item (%s) is not a Content Block.'), utf8_trim( $wrong_item_info ) ) ), $content );
                continue;
            }
            elseif(
get_status_permvalue( $this->get( 'status' ) ) > get_status_permvalue( $content_Item->get( 'status' ) ) )
            {    
// Deny to display content block Item with lower status than parent Item:
                // It means visibility status of content block Item cannot be higher than visibility status of the current/parent Item,
                // See below the ordered list of visibility statuses by weight:
                // - Redirected
                // - Public
                // - Community
                // - Deprecated
                // - Protected
                // - Private
                // - Draft
                // - Review
                // For example, if content block Item has a status "Public" but current/parent Item has a status "Community",
                //              then such content block Item cannot be included into the current/parent Item.
               
$content = str_replace( $source_tag, get_rendering_error( sprintf( T_('The visibility level of the content block "%s" is not sufficient.'), '#'.$content_Item->ID.' '.$content_Item->get( 'urltitle' ) ) ), $content );
                continue;
            }
            elseif(
$content_Item->get( 'creator_user_ID' ) != $this->get( 'creator_user_ID' ) &&
                    ( !
$item_Blog || $content_Item->get( 'creator_user_ID' ) != $item_Blog->get( 'owner_user_ID' ) ) &&
                    ( !
$item_Blog || $content_Item->get_blog_ID() != $item_Blog->ID ) &&
                  ( ! (
$info_Blog = & get_setting_Blog( 'info_blog_ID' ) ) || $content_Item->get_blog_ID() != $info_Blog->ID )
                  )
            {    
// We can display a content block item with at least one condition:
                //  1. Content block Item has same owner as owner of parent Item,
                //  2. Content block Item has same owner as owner of parent Item's collection,
                //  3. Content block Item is in same collection as parent Item,
                //  4. Content block Item from collection for shared content blocks:
               
$content_Blog = & $content_Item->get_Blog();
               
$content = str_replace( $source_tag, get_rendering_error( sprintf(
                   
T_('Content block #%d %s (Coll #%d) (Owner: %s) cannot be included here. It must be in the same collection as including Item (Coll #%d) or the info pages collection (Coll #%d)').'; '.
                   
T_('in any other case, it must have the same owner as the including Item (Item #%d) (Owner: %s) or the same owner as the including Item\'s collection (Owner: %s).'),
                       
$content_Item->ID, '<code>'.$content_Item->get( 'urltitle' ).'</code>', // Content block #%d %s
                       
$content_Item->get_blog_ID(), // (Coll #%d)
                       
get_user_identity_link( NULL, $content_Item->get( 'creator_user_ID' ) ), // (Owner: %s)
                       
$this->get_blog_ID(), // as including Item (Coll #%d)
                       
( $info_Blog = & get_setting_Blog( 'info_blog_ID' ) ) ? $info_Blog->ID : 0, // the info pages collection (Coll #%d)
                       
$this->ID, get_user_identity_link( NULL, $this->get( 'creator_user_ID' ) ), // the including Item (Item #%d) (Owner: %s)
                       
$item_Blog ? get_user_identity_link( NULL, $item_Blog->get( 'owner_user_ID' ) ) : '<code>'.T_('No collection found').'</code>' // the including Item\'s collection (Owner: %s)
                   
) ), $content );
                continue;
            }

            if( ! isset(
$content_block_items ) )
            {    
// Initialize global array to avoid recursion:
               
$content_block_items = array();
            }

            if(
in_array( $content_Item->ID, $content_block_items ) )
            {    
// Replace inline content block tag with error message about recursion:
               
$content = str_replace( $source_tag, get_rendering_error( sprintf( T_('Content inclusion loop detected. Not including "%s".'), '#'.$content_Item->ID.' '.$content_Item->get( 'title' ) ) ), $content );
                continue;
            }

           
// Store current item in global array to avoid recursion:
           
array_unshift( $content_block_items, $content_Item->ID );

           
$option_index = 1;
            if( isset(
$tag_options[ $option_index ] ) &&
               
substr( $tag_options[ $option_index ], 0, 1 ) != '.' )
            {    
// Use easy template from short tag options:
               
$tag_template = $tag_options[ $option_index ];
               
$option_index++;
            }
            else
            {    
// Use default easy template:
               
$tag_template = 'cblock_clearfix';
            }

           
$tag_class = isset( $tag_options[ $option_index ] ) ? trim( $tag_options[ $option_index ] ) : '';
            if(
$tag_class !== '' )
            {    
// If tag has an option with style class
               
$content_block_class = trim( str_replace( array( '.*', '.' ), array( '.'.$item_ID_slug, ' ' ),$tag_class ) );
            }
            else
            {    
// Tag has no class:
               
$content_block_class = '';
            }

           
// Get item content:
           
$current_tag_item_content = $content_Item->get_content_block( array_merge( $params, array(
                   
'template_code'       => $tag_template,
                   
'content_block_class' => $content_block_class,
                ) ) );

           
// Update level inline tags like [---fields:] into [--fields:] in order to make them render by top caller level Item:
           
$current_tag_item_content = $this->update_level_inline_tags( $current_tag_item_content );

            if(
get_param( 'preview' ) === 1 && get_param( 'preview_block' ) === 1 )
            {    
// Display orange debug wrapper around included content-block Item:
                // Item debug info with Title + Slug:
               
$title_debug_info = '<b>'.$content_Item->get( 'title' ).'</b> ('.$content_Item->get( 'urltitle' ).')';
                if(
$item_edit_url = $content_Item->get_edit_url() )
                {    
// Link to edit Item if current User has a permission:
                   
$title_debug_info = '<a href="'.$item_edit_url.'">'.$title_debug_info.'</a>';
                }
               
// Content Template debug info with Name + Code:
               
$TemplateCache = & get_TemplateCache();
                if(
$content_Template = & $TemplateCache->get_localized_by_code( $tag_template, false, false ) )
                {    
// Display template info:
                   
$template_debug_info = '<b>'.$content_Template->get( 'name' ).'</b> ('.$content_Template->get( 'code' ).')';
                    if(
check_user_perm( 'options', 'edit' ) )
                    {    
// Link to edit Template if current User has a permission:
                       
$template_debug_info = '<a href="'.get_admin_url( 'ctrl=templates&amp;action=edit&amp;tpl_ID='.$content_Template->ID ).'">'.$template_debug_info.'</a>';
                    }
                   
$title_debug_info .= ' / '.$template_debug_info;
                }
               
$current_tag_item_content = '<div class="dev-blocks dev-blocks--content-block">'."\n"
                   
.'<div class="dev-blocks-name">'
                       
.$title_debug_info
                   
.'</div>'."\n"
                   
.$current_tag_item_content."\n"
               
.'</div>';
            }

           
// Replace inline content block tag with item content:
           
$content = str_replace( $source_tag, $current_tag_item_content, $content );

           
// Remove current item from global array which is used to avoid recursion:
           
array_shift( $content_block_items );
        }

        return
$content;
    }


   
/**
     * Get content of Item with Item Type usage 'content-block'
     *
     * @param array Params
     * @return string
     */
   
function get_content_block( $params = array() )
    {
        if(
$this->get_type_setting( 'usage' ) != 'content-block' )
        {    
// Exclude no content block Item:
           
return '';
        }

       
// Load for get_skin_setting():
       
load_funcs( 'skins/_skin.funcs.php' );

       
$params = array_merge( array(
               
'template_code' => 'cblock_clearfix',
               
'image_class'   => 'img-responsive',
               
'image_size'    => get_skin_setting( 'main_content_image_size', 'fit-1280x720' ),
               
'image_limit'   =>  1000,
               
'image_link_to' => 'original', // Can be 'original' (image fiel URL), 'single', URL or empty
               
'content_block_class' => '',
            ),
$params );

        return
render_template_code( $params['template_code'], $params, array( 'Item' => $this ) );
    }


   
/**
     * Update level inline tags like [---fields:] into [--fields:] in order to make them render by top caller level Item:
     *
     * @param string Content
     * @param array Params
     * @return string Content
     */
   
function update_level_inline_tags( $content, $params = array() )
    {
        if( isset(
$params['check_code_block'] ) && $params['check_code_block'] && ( ( stristr( $content, '<code' ) !== false ) || ( stristr( $content, '<pre' ) !== false ) ) )
        {    
// Call $this->update_level_inline_tags() on everything outside code/pre:
           
$params['check_code_block'] = false;
           
$content = callback_on_non_matching_blocks( $content,
               
'~<(code|pre)[^>]*>.*?</\1>~is',
                array(
$this, 'update_level_inline_tags' ), array( $params ) );
            return
$content;
        }

       
// Remove one char '-' in order to allow to render the inline tag on top caller Item:
       
$content = preg_replace( '#\[-([\-a-z:]+.*?\])#i', '[$1', $content );

        return
$content;
    }


   
/**
     * Render templates from [template:template_code|param1=value1|param2=value2]
     *
     * @param string Content
     * @param array Params
     * @return string Content
     */
   
function render_templates( $content, $params = array() )
    {
       
$params = array_merge( array(
               
'check_code_block' => true,
               
'render_tag_template' => true,
            ),
$params );

        if( !
$params['render_tag_template'] )
        {    
// No tags for rendering:
           
return $content;
        }

        if(
$params['check_code_block'] && ( ( stristr( $content, '<code' ) !== false ) || ( stristr( $content, '<pre' ) !== false ) ) )
        {    
// Call render_templates() on everything outside code/pre:
           
$params['check_code_block'] = false;
           
$content = callback_on_non_matching_blocks( $content,
               
'~<(code|pre)[^>]*>.*?</\1>~is',
                array(
$this, 'render_templates' ), array( $params ) );
            return
$content;
        }

       
$content = preg_replace_callback( '#\[template:(.+?)\]#is', array( $this, 'render_templates_callback' ), $content );

        return
$content;
    }


   
/**
     * Callback function to render templates
     *
     * @param array Match
     */
   
function render_templates_callback( $m )
    {
       
$params = explode( '|', $m[1], 2 );

       
$TemplateCache = & get_TemplateCache();

        if( ! (
$Template = & $TemplateCache->get_by_code( $params[0], false, false ) ) )
        {    
// Template is not found:
           
return get_rendering_error( 'Template "'.$params[0].'" is not found for <code>'.$m[0].'</code>', 'span' );
        }

        if( isset(
$params[1] ) )
        {    
// Decode params from tag like |param1=value1|param2=value2:
           
$short_tag_params = get_template_tag_params_from_string( $params[1] );
        }
        else
        {    
// No params are provided for the short tag:
           
$short_tag_params = array();
        }

       
// Render template by code:
       
return render_template_code( $params[0], $short_tag_params );
    }


   
/**
     * Render switchable blocks
     *   from [div:.optional.classnames:view=detailed&size=middle]Multiline Content Text[/div]
     *   to <div class="optional classnames" data-display-condition="view=detailed&size=middle" style="display:none">Multiline Content Text</div>
     *
     * @param string Content
     * @param array Params
     * @return string Content
     */
   
function render_switchable_blocks( $content, $params = array() )
    {
       
$params = array_merge( array(
               
'check_code_block' => true,
               
'render_tag_switchable_div' => true,
            ),
$params );

        if( !
$params['render_tag_switchable_div'] )
        {    
// No tags for rendering:
           
return $content;
        }

        if(
$params['check_code_block'] && ( ( stristr( $content, '<code' ) !== false ) || ( stristr( $content, '<pre' ) !== false ) ) )
        {    
// Call render_switchable_content() on everything outside code/pre:
           
$params['check_code_block'] = false;
           
$content = callback_on_non_matching_blocks( $content,
               
'~<(code|pre)[^>]*>.*?</\1>~is',
                array(
$this, 'render_switchable_blocks' ), array( $params ) );
            return
$content;
        }

       
$content = preg_replace_callback( '#(<p>)?\[div:(.+?)\](.*?)\[/div\](</p>)?#is', array( $this, 'render_switchable_blocks_callback' ), $content );

        return
$content;
    }


   
/**
     * Callback function to render switchable content
     *
     * @param array Match
     */
   
function render_switchable_blocks_callback( $m )
    {
       
$params = explode( ':', $m[2] );

       
$div_attrs = array();

        if( isset(
$params[0] ) )
        {    
// Optional classes:
           
$classes = trim( str_replace( '.', ' ', $params[0] ) );
            if(
$classes !== '' )
            {    
// Use only provided classes:
               
$div_attrs['class'] = $classes;
            }
        }

        if( isset(
$params[1] ) )
        {    
// If switchable conditions are provided:
           
$visibility_conditions = $params[1];
           
$div_attrs['data-display-condition'] = $visibility_conditions;
           
// Check visibility conditions:
           
if( ! $this->check_switchable_visibility( $visibility_conditions ) )
            {
               
$div_attrs['style'] = 'display:none';
            }
        }

       
// Fix content which may be wrong rendered by plugin like Auto-P and Markdown because shorttag [div:] is not HTML tag:
        // Trim <br /> tags from begin and end:
       
$div_content = preg_replace( '#^(<br[\s/]*>)?(.+?)(<br[\s/]*>)$#is', '$2', $m[3] );
       
// Balance <p> and </p> tags by moving them from outside [div:] to inside it:
       
$div_content = $m[1].$div_content.( isset( $m[4] ) ? $m[4] : '' );

        return
'<div'.get_field_attribs_as_string( $div_attrs ).'>'.$div_content.'</div>';
    }


   
/**
     * Render switchable content
     *
     * @param string Content
     * @param array Params
     * @return string Content
     */
   
function render_switchable_content( $content, $params = array() )
    {
        if( !
$this->get_type_setting( 'allow_switchable' ) ||
            !
$this->get_setting( 'switchable' ) )
        {    
// Don't render switchable content if it is not allowed by Item Type and disabled for this Item:
           
return $content;
        }

       
$params = array_merge( array(
               
'check_code_block' => true,
            ),
$params );

        if(
$params['check_code_block'] && ( ( stristr( $content, '<code' ) !== false ) || ( stristr( $content, '<pre' ) !== false ) ) )
        {    
// Call render_switchable_content() on everything outside code/pre:
           
$params['check_code_block'] = false;
           
$content = callback_on_non_matching_blocks( $content,
               
'~<(code|pre)[^>]*>.*?</\1>~is',
                array(
$this, 'render_switchable_content' ), array( $params ) );
            return
$content;
        }

       
$content = preg_replace_callback( '#(<[a-z]+.+?)(data-display-condition="(.+?)")(.*?>)#i', array( $this, 'render_switchable_content_callback' ), $content );

        return
$content;
    }


   
/**
     * Callback function to render switchable content
     *
     * @param array Match
     */
   
function render_switchable_content_callback( $m )
    {
        if(
preg_match( '#(^.+ style=")(.+?)(".+)$#i', $m[0], $style_match ) &&
           
stripos( $style_match[2], 'display:' ) !== false )
        {    
// Skip already rendered content, probably by render_switchable_blocks() from short tags [div:]Content[/div]:
           
return $m[0];
        }

        if(
$this->check_switchable_visibility( $m[3] ) )
        {    
// This switchable block should be visible on load current page:
           
return $m[0];
        }
       
// Otherwise hide this switchable block:
       
if( empty( $style_match ) )
        {    
// Add new style attribute:
           
return $m[1].$m[2].' style="display:none;"'.$m[4];
        }
        else
        {    
// Append style property to existing attribute:
           
return $style_match[1].trim( $style_match[2], '; ' ).';display:none;"'.$style_match[3];
        }
    }


   
/**
     * Check if block/row/field can be visible by requested conditions
     *
     * @param string Conditions, e.g. view=detailed&size=middle
     * @return boolean TRUE if block/row/field can be visible, FALSE if it must be hidden
     */
   
function check_switchable_visibility( $conditions )
    {
       
$disp_conditions = explode( '&', str_replace( array( '&amp;amp;', '&amp;' ), '&', $conditions ) );

        foreach(
$disp_conditions as $disp_condition )
        {
           
$disp_condition = explode( '=', $disp_condition );
           
// Get all allowed value by the condition of the custom field:
           
$disp_condition_values = explode( '|', $disp_condition[1] );
           
// Get current value of the param from $_GET or $_POST:
           
$param_value = param( $disp_condition[0], 'string' );
           
// Check if we should hide the custom field by condition:
           
if( ( $param_value === '' && ! in_array( $this->get_switchable_param( $disp_condition[0] ), $disp_condition_values ) ) || // current param value is empty but condition doesn't allow empty values
               
! preg_match( '/^[a-z0-9_\-]*$/', $param_value ) || // wrong param value
               
( $param_value !== '' && ! in_array( $param_value, $disp_condition_values ) ) ) // current param value is not allowed by the condition of the custom field
           
{    // Hide custom field if at least one param is not allowed by condition of the custom field:
               
return false;
            }
        }

        return
true;
    }


   
/**
     * Load switchable params
     */
   
function load_switchable_params()
    {
        if( !
$this->get_type_setting( 'allow_switchable' ) ||
            !
$this->get_setting( 'switchable' ) )
        {    
// Don't render switchable content if it is not allowed by Item Type and disabled for this Item:
           
$this->switchable_params = array();
            return;
        }

        if( isset(
$this->switchable_params ) )
        {    
// Don't initialize params twice:
           
return;
        }

       
$this->switchable_params = array();

       
// Keep additional param codes in the URL:
       
$url_param_codes = $this->get_setting( 'switchable_params' );
        if( ! empty(
$url_param_codes ) )
        {
           
$url_param_codes = explode( ',', $url_param_codes );
            foreach(
$url_param_codes as $url_param_code )
            {
               
$url_param_code = explode( '=', trim( $url_param_code ) );
                if( ! empty(
$url_param_code[0] ) )
                {    
// Memorize additional param to regenerate proper URL below:
                   
$default_value = ( isset( $url_param_code[1] ) ? $url_param_code[1] : '' );
                   
$url_param_value = param( $url_param_code[0], 'string', '', true );
                    if(
$url_param_value === '' )
                    {    
// Memorize and set default value as default:
                       
memorize_param( $url_param_code[0], 'string', '', $default_value );
                       
set_param( $url_param_code[0], $default_value );
                    }
                   
$this->switchable_params[ $url_param_code[0] ] = $default_value;
                }
            }
        }
    }


   
/**
     * Get switchable params
     *
     * @return array Switchable params: Key - param code, Value - default param value
     */
   
function get_switchable_params()
    {
       
$this->load_switchable_params();

        return
$this->switchable_params;
    }


   
/**
     * Get switchable param by code
     *
     * @param string Param code
     * @return string|NULL Param value
     */
   
function get_switchable_param( $param_code )
    {
       
$this->load_switchable_params();

        return ( isset(
$this->switchable_params[ $param_code ] ) ? $this->switchable_params[ $param_code ] : NULL );
    }


   
/**
     * Display "more" link to "After more" or follow-up anchor
     */
   
function more_link( $params = array() )
    {
        echo
$this->get_more_link( $params );
    }


   
/**
     * Get "more" link to "After more" or follow-up anchor
     */
   
function get_more_link( $params = array() )
    {
       
// Make sure we are not missing any param:
       
$params = array_merge( array(
               
'force_more'  => false,
               
'before'      => '<p class="bMore">',
               
'after'       => '</p>',
               
'link_text'   => '#',        // text to display as the more link
               
'anchor_text' => '#',        // text to display as the more anchor (once the more link has been clicked, # defaults to "Follow up:")
               
'link_to'     => 'single#anchor',    // target URL for more link, 'single' or 'single#anchor'
               
'disppage'    => '#',        // page number to display specific page, # for url parameter
               
'format'      => 'htmlbody',
               
'link_class'  => '', // class name of the link
               
'force_hide_teaser' => false, // Force an item setting 'hide_teaser'
           
), $params );

        global
$more;

        if( !
$this->has_content_parts($params) )
        {
// This is NOT an extended post, no "read more" is needed:
           
return '';
        }

       
/* fp 2020-02-22: obsolete code
        if( ( $more == 0 ) && ( $params[ 'link_to' ] == false ) )
        { // Don't display "After more" content
            if( !empty( $params[ 'link_text' ] ) )
            {
                return format_to_output( $params[ 'before'].$params[ 'link_text' ].$params[ 'after'] );
            }
            return '';
        }
        */

       
$content_parts = $this->get_content_parts($params);

       
// Init an attribute for class
       
$class_attr = empty( $params['link_class'] ) ? '' : ' class="'.$params['link_class'].'"';

        if( !
$more && ! $params['force_more'] )
        {    
// We're NOT in "more" mode:
           
if( $params['link_text'] == '#' )
            {
// TRANS: this is the default text for the extended post "more" link
               
$params['link_text'] = T_('Read more').' &raquo;';
               
// Dummy in order to keep previous translation in the loop:
               
$dummy = T_('Full story');
            }

            switch(
$params['link_to'] )
            {
                case
'single':
                   
$params['link_to'] = $this->get_permanent_url();
                    break;

                case
'single#anchor':
                   
$params['link_to'] = $this->get_permanent_url().'#more'.$this->ID;
                    break;
            }

            return
format_to_output( $params['before']
                        .
'<a href="'.$params['link_to'].'"'.$class_attr.'>'
                       
.$params['link_text'].'</a>'
                       
.$params['after'], $params['format'] );
        }
        elseif( !
$params['force_hide_teaser'] && ! $this->get_setting( 'hide_teaser' ) )
        {    
// We are in more mode and we're not hiding the teaser:
            // (if we're hiding the teaser we display this as a normal page ie: no anchor)
           
if( $params['anchor_text'] == '#' )
            {
// TRANS: this is the default text displayed once the more link has been activated
               
$params['anchor_text'] = '<p class="bMore">'.T_('Follow up:').'</p>';
            }

            return
format_to_output( '<a id="more'.$this->ID.'" name="more'.$this->ID.'"'.$class_attr.'></a>'
                           
.$params['anchor_text'], $params['format'] );
        }
    }


   
/**
     * Does the post have different content parts (teaser/extension, divided by "[teaserbreak]")?
     * This is also true for posts that have images with "aftermore" position.
     *
     * @todo fp> This is a heavy operation! We should probably store the presence of `[teaserbreak]` in a var so that future cals do not rexecute again.
     *           BUT first we need to know why we're interested in $params['disppage'], $params['format']  (or better said: in wgat case are we using different values for this?)
     *           ALSO we should probably store the position of [teaserbreak] for even better performance
     *           ALSO we may want to store that at UPDATE time, into the DB, so we have super fast access to it.
     *
     * @access public
     * @return boolean
     */
   
function has_content_parts( $params )
    {
       
// Make sure we are not missing any param:
       
$params = array_merge( array(
               
'disppage' => '#',
               
'format'   => 'htmlbody',
            ),
$params );

        if( ! isset(
$this->cache_has_content_parts ) )
        {    
// Initialize an array for cache results:
           
$this->cache_has_content_parts = array();
        }

        if( ! isset(
$this->cache_has_content_parts[ $params['disppage'].$params['format'] ] ) )
        {    
// Initialize result only first time and store in cache in order to don't execute a heavy operation twice:
           
$content_page = $this->get_content_page( $params['disppage'], $params['format'] );

           
// Replace <code> and <pre> blocks from content because we're not interested in [teaserbreak] in there
           
$content_page = preg_replace( '~<(code|pre)[^>]*>.*?</\1>~is', '*', $content_page );

           
// Store result in cache for requested page and format:
           
$this->cache_has_content_parts[ $params['disppage'].$params['format'] ] =
                   
strpos( $content_page, '[teaserbreak]' ) !== false
               
|| $this->get_images( array( 'restrict_to_image_position' => 'aftermore' ) );
        }

       
// Get a result from cache or from recently initialized var above:
       
return $this->cache_has_content_parts[ $params['disppage'].$params['format'] ];
    }


   
/**
     * Template function: display deadline date (datetime) of Item
     *
     * @param string date/time format: leave empty to use locale default date format
     * @param boolean true if you want GMT
     */
   
function deadline_date( $format = '', $useGM = false )
    {
        if( empty(
$format) )
            echo
mysql2date( locale_datefmt(), $this->datedeadline, $useGM);
        else
            echo
mysql2date( $format, $this->datedeadline, $useGM);
    }


   
/**
     * Template function: display deadline time (datetime) of Item
     *
     * @param string date/time format: leave empty to use locale default time format
     * @param boolean true if you want GMT
     */
   
function deadline_time( $format = '', $useGM = false )
    {
        if( empty(
$format) )
            echo
mysql2date( locale_shorttimefmt(), $this->datedeadline, $useGM );
        else
            echo
mysql2date( $format, $this->datedeadline, $useGM );
    }


   
/**
     * Get the title for the <title> tag
     *
     * If it's not specifically entered, use the regular post title instead
     */
   
function get_titletag()
    {
        if( empty(
$this->titletag ) )
        {
            return
$this->get( 'title' );
        }

        return
$this->titletag;
    }

   
/**
     * Get the meta description tag
     *
     */
   
function get_metadesc()
    {
        return
$this->get_setting( 'metadesc' );
    }

   
/**
     * Get the meta keyword tag
     *
     */
   
function get_metakeywords()
    {
        return
$this->get_setting( 'metakeywords' );
    }


   
/**
     * Split tags by comma or semicolon
     *
     * @param string The tags, separated by comma or semicolon
     */
   
function set_tags_from_string( $tags )
    {
       
// Mark that tags has been updated, even if it is not sure because we do not want to execute extra db query
       
$this->dbchanges_flags['tags'] = true;

        if(
$tags === '' )
        {
           
$this->tags = array();
            return;
        }

       
$this->tags = preg_split( '/\s*[;,]+\s*/', $tags, -1, PREG_SPLIT_NO_EMPTY );
        foreach(
$this->tags as $t => $tag )
        {
            if(
substr( $tag, 0, 1 ) == '-' )
            {
// Prevent chars '-' in first position
               
$tag = preg_replace( '/^-+/', '', $tag );
            }
            if( empty(
$tag ) )
            {
// Don't save empty tag
               
unset( $this->tags[ $t ] );
            }
            else
            {
// Save the modifications for each tag
               
$this->tags[ $t ] = $tag;
            }
        }

       
// Remove the duplicate tags
       
$this->tags = array_unique( $this->tags );
    }


   
/**
     * Template function: Provide link to message form for this Item's author.
     *
     * @param string url of the message form
     * @param string to display before link
     * @param string to display after link
     * @param string link text
     * @param string link title
     * @param string class name
     * @return boolean true, if a link was displayed; false if there's no email address for the Item's author.
     */
   
function msgform_link( $params = array() )
    {
       
// Make sure we are not missing any param:
       
$params = array_merge( array(
               
'before'      => ' ',
               
'after'       => ' ',
               
'text'        => '#',
               
'title'       => '#',
               
'class'       => '',
               
'format'      => 'htmlbody',
               
'form_url'    => '#current_blog#',
            ),
$params );


        if(
$params['form_url'] == '#current_blog#' )
        {    
// Get
           
global $Collection, $Blog;
           
$params['form_url'] = $Blog->get('msgformurl');
        }

       
$this->get_creator_User();
       
$redirect_to = url_add_param( $params['form_url'], 'post_id='.$this->ID.'&recipient_id='.$this->creator_User->ID, '&' );
       
$params['form_url'] = $this->creator_User->get_msgform_url( url_add_param( $params['form_url'], 'post_id='.$this->ID ), $redirect_to );

        if( empty(
$params['form_url'] ) )
        {
            return
false;
        }

        if(
$params['title'] == '#' )
        {
            if(
$this->creator_User->get_msgform_possibility() == 'email' )
            {
               
$params['title'] = T_('Send email to post author');
            }
            else
            {
               
$params['title'] = T_('Send message to post author');
            }
        }
        if(
$params['text'] == '#' ) $params['text'] = get_icon( 'email', 'imgtag', array( 'class' => 'middle', 'title' => $params['title'] ) );

        echo
$params['before'];
        echo
'<a href="'.$params['form_url'].'" title="'.$params['title'].'"';
        if( !empty(
$params['class'] ) ) echo ' class="'.$params['class'].'"';
        echo
' rel="nofollow">'.$params['text'].'</a>';
        echo
$params['after'];

        return
true;
    }


   
/**
     * Template function: Provide link to message form for this Item's assigned User.
     *
     * @param string url of the message form
     * @param string to display before link
     * @param string to display after link
     * @param string link text
     * @param string link title
     * @param string class name
     * @return boolean true, if a link was displayed; false if there's no email address for the assigned User.
     */
   
function msgform_link_assigned( $form_url, $before = ' ', $after = ' ', $text = '#', $title = '#', $class = '' )
    {
        if( !
$this->get_assigned_User() || empty($this->assigned_User->email) )
        {
// We have no email for this Author :(
           
return false;
        }

       
$form_url = url_add_param( $form_url, 'recipient_id='.$this->assigned_User->ID );
       
$form_url = url_add_param( $form_url, 'post_id='.$this->ID );

        if(
$title == '#' ) $title = T_('Send email to assigned user');
        if(
$text == '#' ) $text = get_icon( 'email', 'imgtag', array( 'class' => 'middle', 'title' => $title ) );

        echo
$before;
        echo
'<a href="'.$form_url.'" title="'.$title.'"';
        if( !empty(
$class ) ) echo ' class="'.$class.'"';
        echo
' rel="nofollow">'.$text.'</a>';
        echo
$after;

        return
true;
    }


   
/**
     * Display a link to pages for multi-page items
     *
     * @param array of params
     * @param string Output format, see {@link format_to_output()}
     */
   
function page_links()
    {
        global
$preview;

       
$num_args = func_num_args();
       
$args = func_get_args();

        if(
$num_args == 1 && is_array($args[0]) )
        {
           
$params = $args[0];
            if( !isset(
$params['format']) ) $params['format'] = 'htmlbody';
        }
        else
        {    
// Deprecated since v5, left for compatibility with old skins
           
$params['before']        = isset($args[0]) ? $args[0] : '<p class="evo_post_pagination">'.T_('Pages').': ';
           
$params['after']        = isset($args[1]) ? $args[1] : '</p>';
           
$params['separator']    = isset($args[2]) ? $args[2] : ' ';
           
$params['single']        = isset($args[3]) ? $args[3] : '';
           
$params['current_page']    = isset($args[4]) ? $args[4] : '#';
           
$params['pagelink']        = isset($args[5]) ? $args[5] : '%d';
           
$params['url']            = isset($args[6]) ? $args[6] : '';
           
$params['format']        = 'htmlbody';
        }

        if(
$preview && $this->pages > 1 && ! $this->ID )
        {
// Do not render page links if it is a preview of a unsaved multipage item
           
echo '';
        }
        else
        {
            echo
$this->get_page_links( $params, $params['format'] );
        }
    }


   
/**
     * Get a link to pages for multi-page items
     *
     * @param array of params
     * @param string Output format, see {@link format_to_output()}
     */
   
function get_page_links( $params = array(), $format = 'htmlbody' )
    {
       
$params = array_merge( array(
                   
'before'       => '<p class="evo_post_pagination">'.T_('Pages').': ',
                   
'after'        => '</p>',
                   
'separator'    => ' ',
                   
'single'       => '',
                   
'current_page' => '#',
                   
'pagelink'     => '%d',
                   
'url'          => '',
                ),
$params );

        global
$disp;

       
// Make sure, the pages are split up:
       
$this->split_pages();

        if(
$this->pages <= 1 )
        {
// Single page:
           
return $params['single'];
        }

        if(
$params['separator'] == NULL )
        {
// Don't display pages
           
if( $params['before'] !== NULL )
            {
                return
format_to_output( $params['before'].$params['after'], $format );
            }
            return;
        }

        if(
$params['current_page'] == '#' )
        {
            global
$page;
           
$params['current_page'] = $page;
        }

        if( empty(
$params['url']) )
        {
           
$params['url'] = $this->get_permanent_url( '', '', '&amp;' );
        }

       
$page_links = array();

       
$page_prev_i = $params['current_page'] - 1;
       
$page_next_i = $params['current_page'] + 1;
        for(
$i = 1; $i <= $this->pages; $i++ )
        {
           
$text = str_replace('%d', $i, $params['pagelink']);

            if(
$i != $params['current_page'] )
            {
               
$attr_rel = '';
                if(
$disp == 'single' || $disp == 'page' )
                {
// Add rel="prev|next" only on single post view
                   
if( $page_prev_i == $i )
                    {
                       
$attr_rel = ' rel="prev"';
                    }
                    elseif(
$page_next_i == $i )
                    {
                       
$attr_rel = ' rel="next"';
                    }
                }

                if(
$i == 1 )
                {    
// First page special:
                   
$page_links[] = '<a href="'.$params['url'].'"'.$attr_rel.'>'.$text.'</a>';
                }
                else
                {
                   
$page_links[] = '<a href="'.url_add_param( $params['url'], 'page='.$i ).'"'.$attr_rel.'>'.$text.'</a>';
                }
            }
            else
            {
               
$page_links[] = $text;
            }
        }

       
$r = $params['before'].implode( $params['separator'], $page_links ).$params['after'];

        return
format_to_output( $r, $format );
    }


   
/**
     * Get an attached image tag with lightbox reference
     *
     * @private function
     *
     * @param object the attached image Link
     * @param array params
     * @return string the attached image tag
     */
   
function get_attached_image_tag( $Link, $params )
    {
       
// Make sure $link_to is set
       
$link_to = isset( $params['image_link_to'] ) ? $params['image_link_to'] : 'original';

       
// Force url of image for link positions: 'teaserperm' & 'teaserlink'
       
switch( $Link->get( 'position' ) )
        {
            case
'teaserlink':
               
// Teaser-Ext Link
               
if( $this->get( 'url' ) != '' )
                {
// Only when post field 'Link to url' is defined
                   
$link_to = 'url';
                    break;
                }
               
// If post url is empty then use this link position as 'teaserperm':

           
case 'teaserperm':
            case
'cover':
            case
'background':
               
// Teaser-Permalink or Cover
               
global $disp;
                if( isset(
$disp ) && $disp == 'single' )
                {
// Force link to image url and use colorbox only when we already on permalink page
                   
$link_to = 'original';
                }
                else
                {
// Force link to permalink of this post
                   
$link_to = 'single';
                }
                break;
        }

        if(
$link_to == 'single' || $link_to == 'url' )
        {
// We're linking to the post (displayed on a single post page):
           
if( $link_to == 'url' && $this->get( 'url' ) != '' )
            {
// Link to url from the post field 'Link to url'
               
$link_to = $this->get( 'url' );
            }
            else
            {
// Link to Item url:
               
$params = array_merge( array(  
                       
'target_blog'     => '',        
                       
'post_navigation' => '',        
                       
'nav_target'      => NULL,        
                    ),
$params );

               
$link_to = $this->get_item_url( $params['target_blog'], $params['post_navigation'], $params['nav_target'] );
            }
           
$link_title = '#desc#';
           
$link_rel = isset( $params['image_link_rel'] ) ? $params['image_link_rel'] : '';
        }
        else
        {
// We're linking to the original image, let lightbox (or clone) kick in:
           
$link_title =  ( empty( $params['image_link_title'] ) && !isset( $params['hide_image_link_title'] ) ) ? '#desc#' : $params['image_link_title'];    // This title will be used by lightbox (colorbox for instance)
           
$link_rel = isset( $params['image_link_rel'] ) ? $params['image_link_rel'] : 'lightbox[p'.$this->ID.']';    // Make one "gallery" per post.
       
}

        if( empty(
$params['image_alt'] ) )
        {    
// Override image alt text by current Item title only when it is not passed e.g. from inline/short tag `[image:123::Custom Alt Text]`:
           
$params['image_alt'] = $this->get( 'title' );
        }

       
// Generate the IMG tag with all the alt, title and desc if available
       
return $Link->get_tag( array_merge( $params, array(
                   
'image_link_to'    => $link_to,   // can be URL, can be empty
                   
'image_link_title' => $link_title,
                   
'image_link_rel'   => $link_rel,
                ) ) );
    }


   
/**
     * Display the images linked to the current Item
     *
     * @param array of params
     * @param string Output format, see {@link format_to_output()}
     */
   
function images( $params = array(), $format = 'htmlbody' )
    {
        echo
$this->get_images( $params, $format );
    }


   
/**
     * Get block of images linked to the current Item
     *
     * @param array of params
     * @param string Output format, see {@link format_to_output()}
     */
   
function get_images( $params = array(), $format = 'htmlbody' )
    {
        global
$Plugins;

       
$r = '';

       
$params = array_merge( array(
               
'before'                     => '<div>',
               
'before_image'               => '<div class="image_block">',
               
'before_image_classes'       => '', // Allow injecting additional classes into 'before image'
               
'before_image_legend'        => '<div class="image_legend">',
               
'after_image_legend'         => '</div>',
               
'after_image'                => '</div>',
               
'after'                      => '</div>',
               
'image_size'                 => 'fit-720x500',
               
'image_size_x'               => 1, // Use '2' to build 2x sized thumbnail that can be used for Retina display
               
'image_sizes'                => NULL, // Simplified "sizes=" attribute for browser to select correct size from "srcset=".
                                                            // Must be set DIFFERENTLY depending on WIDGET/CONTAINER/SKIN LAYOUT. Each time we must estimate the size the image will have on screen.
                                                            // Sample value: (max-width: 430px) 400px, (max-width: 670px) 640px, (max-width: 991px) 720px, (max-width: 1199px) 698px, 848px
               
'image_link_to'              => 'original', // Can be 'original' (image file URL), 'single' (this post), can be URL, can be EMPTY
                    // In case of 'single' link:
                   
'target_blog'             => '',        
                   
'post_navigation'         => '',        
                   
'nav_target'              => NULL,        
               
'before_gallery'             => '<div class="bGallery">',
               
'after_gallery'              => '</div>',
               
'gallery_image_size'         => 'crop-80x80',
               
'gallery_image_limit'        => 1000,
               
'gallery_colls'              => 5,
               
'gallery_order'              => '', // 'ASC', 'DESC', 'RAND'
               
'gallery_link_rel'           => 'lightbox[p'.$this->ID.']',
               
'restrict_to_image_position' => 'teaser,teaserperm,teaserlink,aftermore',
                                                               
// 'teaser'|'teaserperm'|'teaserlink'|'aftermore'|'inline'|'cover'|'background',
                                                                // '#teaser_all' => 'teaser,teaserperm,teaserlink',
                                                                // '#cover_and_teaser_all' => 'cover,background,teaser,teaserperm,teaserlink'
               
'limit'                      => 1000, // Max # of images displayed
               
'placeholder'                => '',        // HTML to be displayed if no image; possible codes: #folder_icon
               
'data'                       =>  & $r,
               
'get_rendered_attachments'   => true,
               
'links_sql_select'           => '',
               
'links_sql_orderby'          => 'link_order',
            ),
$params );

        if( ! empty(
$params['before_image_classes'] ) )
        {    
// Inject additional classes into 'before image':
           
$params['before_image'] = update_html_tag_attribs( $params['before_image'], array( 'class' => $params['before_image_classes'] ) );
        }

       
// Get list of ALL attached files
       
$links_params = array(
               
'sql_select_add' => $params['links_sql_select'],
               
'sql_order_by'   => $params['links_sql_orderby']
            );

       
// Set image positions from possible predefined values:
       
switch( $params['restrict_to_image_position'] )
        {
            case
'#teaser_all':
               
$params['restrict_to_image_position'] = 'teaser,teaserperm,teaserlink';
                break;
            case
'#cover_and_teaser_all':
               
$params['restrict_to_image_position'] = 'cover,background,teaser,teaserperm,teaserlink';
                break;
        }

        if( empty(
$this->ID ) )
        {    
// Preview mode for new creating item:
           
$tmp_object_ID = param( 'temp_link_owner_ID', 'integer', 0 );
        }
        else
        {    
// Normal mode for existing Item in DB:
           
$tmp_object_ID = NULL;
        }

       
// GET list of images to display:
       
$LinkOwner = new LinkItem( $this, $tmp_object_ID );
        if( !
$LinkList = $LinkOwner->get_attachment_LinkList( 1000, $params['restrict_to_image_position'], NULL, $links_params ) )
        {    
// No images match requested positions:

            // Display PLACEHOLDER:
           
$placeholder_html = $params['placeholder'];
            switch(
$placeholder_html )
            {
                case
'#file_text_icon';
                   
$placeholder_html = '<div class="evo_image_block evo_img_placeholder"><a href="$url$" class="evo_img_placeholder"><i class="fa fa-file-text-o"></i></a></div>';
                    break;
                case
'#file_thumbnail_text_icon';
                   
$placeholder_html = '<div class="evo_thumblist_placeholder" style="width:80px;height:80px"><a href="$url$"></a></div>';
                    break;
            }
            return
str_replace( '$url$', $this->get_item_url( $params['target_blog'], $params['post_navigation'], $params['nav_target'] ), $placeholder_html );
        }

       
// LOOP through images:
       
$galleries = array();
       
$image_counter = 0;
       
$plugin_render_attachments = false;
        while(
$image_counter < $params['limit'] && $Link = & $LinkList->get_next() )
        {
            if( ! (
$File = & $Link->get_File() ) )
            {
// No File object:
               
global $Debuglog;
               
$log_message = sprintf( 'Link ID#%d of item #%d does not have a file object!', $Link->ID, $this->ID );
                if(
$this->is_revision() )
                {    
// Display the log message only for revision preview mode:
                   
$r .= '<div class="red">'.$log_message.'</div>';
                }
               
$Debuglog->add( $log_message, array( 'error', 'files' ) );
                continue;
            }

            if( !
$File->exists() )
            {
// File doesn't exist:
               
global $Debuglog;
               
$log_message = sprintf( 'File linked to item #%d does not exist (%s)!', $this->ID, $File->get_full_path() );
                if(
$this->is_revision() )
                {    
// Display the log message only for revision preview mode:
                   
$r .= '<div class="red">'.$log_message.'</div>';
                }

               
// Still generate the IMG tag but this should display a black thumbnail with the appropriate error message:
               
$r .= $this->get_attached_image_tag( $Link, $params );

               
$image_counter++;

               
$Debuglog->add( $log_message, array( 'error', 'files' ) );
                continue;
            }

           
$params['File'] = $File;
           
$params['Link'] = $Link;
           
$params['Item'] = $this;

            if(
$File->is_dir() && $params['gallery_image_limit'] > 0 )
            {
// This is a directory/gallery:
               
if( ( $gallery = $File->get_gallery( $params ) ) != '' )
                {
// Got gallery code
                   
$galleries[] = $gallery;
                }
                continue;
            }

            if( !
$params['get_rendered_attachments'] )
            {
// Save $r to temp var in order not to get the rendered data from plugins
               
$temp_r = $r;
            }

           
$temp_params = $params;
            foreach(
$params as $param_key => $param_value )
            {    
// Pass all params by reference, in order to give possibility to modify them by plugin
                // So plugins can add some data before/after image tags (E.g. used by infodots plugin)
               
$params[ $param_key ] = & $params[ $param_key ];
            }

           
// Prepare params before rendering item attachment:
           
$Plugins->trigger_event_first_true_with_params( 'PrepareForRenderItemAttachment', $params );

            if(
count( $Plugins->trigger_event_first_true( 'RenderItemAttachment', $params ) ) != 0 )
            {    
// This attachment has been rendered by a plugin (to $params['data']), Skip this from core rendering:
               
if( ! $params['get_rendered_attachments'] )
                {
// Restore $r value and mark this item has the rendered attachments
                   
$r = $temp_r;
                   
$plugin_render_attachments = true;
                }
                continue;
            }

            if( !
$File->is_image() )
            {    
// Skip anything that is not an image:
                //$r .= $this->attachment_files($params);
               
continue;
            }

           
// GENERATE the IMG tag with all the alt, title and desc if available:
           
$r .= $this->get_attached_image_tag( $Link, $params );

           
$image_counter++;
           
$params = $temp_params;
        }

        if( empty(
$r ) && $plugin_render_attachments )
        {
// This item doesn't contain the images but it has the rendered attachments by plugins
           
$r .= 'plugin_render_attachments';
        }

        if( !empty(
$r) )
        {
           
$r = $params['before'].$r.$params['after'];

           
// Character conversions
           
$r = format_to_output( $r, $format );
        }

        if( !empty(
$galleries) )
        {
// Append galleries
            // sam2kb> It's done like that only until we figure out a better way to display galleries.

            /*
            sam2kb> TODO: use shortcode [gallery option1="value1" option2="value2"]
                'columns' - table columns
                'limit' - a number of images,
                'size' - selected/large image size
                'thumbsize' - thumbnails image size
                'order' - files order ASC/DESC/RAND
            */

            // Character conversions
           
$r .= "\n".format_to_output( implode("\n", $galleries), $format );
        }

        return
$r;
    }


   
/**
     * Get File of a first found image by positions
     *
     * @param array Parameters
     * @return object|NULL File
     */
   
function & get_image_File( $params = array() )
    {
       
$params = array_merge( array(
               
'position' => '#cover_and_teaser_all',
            ),
$params );

       
// Set image positions from possible predefined values:
       
switch( $params['position'] )
        {
            case
'#teaser_all':
               
$params['position'] = 'teaser,teaserperm,teaserlink';
                break;
            case
'#cover_and_teaser_all':
               
$params['position'] = 'cover,background,teaser,teaserperm,teaserlink';
                break;
        }

       
$LinkOwner = new LinkItem( $this );
        if( ! (
$LinkList = $LinkOwner->get_attachment_LinkList( 1, $params['position'] ) ) ||
            ! (
$Link = & $LinkList->get_next() ) )
        {    
// No image
           
$r = NULL;
            return
$r;
        }

        if( ! (
$File = & $Link->get_File() ) )
        {    
// No File object
           
global $Debuglog;
           
$Debuglog->add( sprintf( 'Link ID#%d of item #%d does not have a file object!', $Link->ID, $this->ID ), array( 'error', 'files' ) );
           
$r = NULL;
            return
$r;
        }

        if( !
$File->exists() )
        {    
// File doesn't exist
           
global $Debuglog;
           
$Debuglog->add( sprintf( 'File linked to item #%d does not exist (%s)!', $this->ID, $File->get_full_path() ), array( 'error', 'files' ) );
           
$r = NULL;
            return
$r;
        }

        if( !
$File->is_image() )
        {    
// Skip anything that is not an image
           
$r = NULL;
            return
$r;
        }

        return
$File;
    }


   
/**
     * Get URL of a first found image by positions
     *
     * @param array Parameters
     * @return string|NULL Image URL or NULL if it doesn't exist
     */
   
function get_image_url( $params = array() )
    {
       
$params = array_merge( array(
               
'position' => '#cover_and_teaser_all',
               
'size'     => 'original',
            ),
$params );

        if( ! (
$image_File = & $this->get_image_File( $params ) ) )
        {    
// Wrong image file:
           
return NULL;
        }

       
// Get image URL for requested size:
       
$img_attribs = $image_File->get_img_attribs( $params['size'] );

        return
$img_attribs['src'];
    }


   
/**
     * Get URL of a first cover image
     *
     * @param string Restrict to files/images linked to a specific position.
     *               Position can be 'cover'|'background'|'teaser'|'aftermore'|'inline'
     *               Use comma as separator
     * @return string|NULL cover URL or NULL if it doesn't exist
     */
   
function get_cover_image_url( $position = 'cover' )
    {
        return
$this->get_image_url( array( 'position' => $position ) );
    }


   
/**
     * Get CSS property for background with image of this Item
     *
     * @param array Params
     * @return string
     */
   
function get_background_image_css( $params = array() )
    {
       
$params = array_merge( array(
               
'position' => '#cover_and_teaser_all',
               
'size'     => 'fit-1280x720',
               
'size_2x'  => 'fit-2560x1440',
            ),
$params );

        if( ! (
$image_File = & $this->get_image_File( $params ) ) )
        {    
// Don't provide css for wrong image file:
           
return '';
        }

        return
$image_File->get_background_image_css( $params );
    }


   
/**
     * Get a number of images linked to the current Item
     *
     * @param string Restrict to files/images linked to a specific position.
     *               Position can be 'teaser'|'teaserperm'|'teaserlink'|'aftermore'|'inline'|'cover'|'background'
     *               Use comma as separator
     * @param integer Number of images
     */
   
function get_number_of_images( $image_position = NULL )
    {
       
// Get list of attached files
       
$LinkOwner = new LinkItem( $this );
        if( !
$LinkList = $LinkOwner->get_attachment_LinkList( 1000, $image_position ) )
        {
            return
0;
        }

        return
$LinkList->result_num_rows;
    }


   
/**
     * Display the attachments/files linked to the current Item
     *
     * @param array Array of params
     * @param string Output format, see {@link format_to_output()}
     */
   
function files( $params = array(), $format = 'htmlbody' )
    {
        echo
$this->get_files( $params, $format );
    }


   
/**
     * Get block of attachments/files linked to the current Item
     *
     * @param array Array of params
     * @param string Output format, see {@link format_to_output()}
     * @return string HTML
     */
   
function get_files( $params = array(), $format = 'htmlbody' )
    {
        global
$Plugins;
       
$params = array_merge( array(
               
'before' =>              '<div class="item_attachments"><h3>'.T_('Attachments').':</h3><ul class="bFiles">',
               
'before_attach' =>         '<li>',
               
'before_attach_size' =>    '<span class="file_size">(',
               
'after_attach_size' =>     ')</span>',
               
'after_attach' =>          '</li>',
               
'after' =>               '</ul></div>',
           
// fp> TODO: we should only have one limit param. Or is there a good reason for having two?
            // sam2kb> It's needed only for flexibility, in the meantime if user attaches 200 files he expects to see all of them in skin, I think.
               
'limit_attach' =>        1000, // Max # of files displayed
               
'limit' =>               1000,
               
// Optionally restrict to files/images linked to specific position: 'teaser'|'teaserperm'|'teaserlink'|'aftermore'|'inline'|'cover'|'background'
               
'restrict_to_image_position' => 'cover,background,teaser,teaserperm,teaserlink,aftermore,attachment',
               
'data'                       => '',
               
'attach_format'              => '$icon_link$ $file_link$ $file_size$ $file_desc$', // $icon_link$ $icon$ $file_link$ $file_size$ $file_desc$
               
'file_link_format'           => '$file_name$', // $icon$ $file_name$ $file_size$ $file_desc$
               
'file_link_class'            => '',
               
'file_link_text'             => 'filename', // 'filename' - Always display Filename, 'title' - Display Title if available
               
'download_link_icon'         => 'download',
               
'download_link_title'        => T_('Download file'),
               
'display_download_icon'      => true,
               
'display_file_size'          => true,
               
'display_file_desc'          => false,
               
'before_file_desc'           => '<span class="evo_file_description">',
               
'after_file_desc'            => '</span>',
            ),
$params );

       
// Get list of attached files
       
$LinkOwner = new LinkItem( $this );
        if( !
$LinkList = $LinkOwner->get_attachment_LinkList( $params['limit'], $params['restrict_to_image_position'] ) )
        {
            return
'';
        }

       
load_funcs('files/model/_file.funcs.php');

       
$r = '';
       
$i = 0;
       
$r_file = array();
       
/**
         * @var File
         */
       
$File = NULL;
        while( (
$Link = & $LinkList->get_next() ) && $params['limit_attach'] > $i )
        {
            if(
$Link->get( 'position' ) != 'attachment' )
            {    
// Skip not "attachment" links:
               
continue;
            }

            if( ! (
$File = & $Link->get_File() ) )
            {
// No File object
               
global $Debuglog;
               
$log_message = sprintf( 'Link ID#%d of item #%d does not have a file object!', $Link->ID, $this->ID );
                if(
$this->is_revision() )
                {    
// Display the log message only for revision preview mode:
                   
$r_file[$i] = $params['before_attach'].'<div class="red">'.$log_message.'</div>'.$params['after_attach'];
                }
               
$Debuglog->add( $log_message, array( 'error', 'files' ) );
                continue;
            }

            if( !
$File->exists() )
            {
// File doesn't exist
               
global $Debuglog;
               
$log_message = sprintf( 'File linked to item #%d does not exist (%s)!', $this->ID, $File->get_full_path() );
                if(
$this->is_revision() )
                {    
// Display the log message only for revision preview mode:
                   
$r_file[$i] = $params['before_attach'].'<div class="red">'.$log_message.'</div>'.$params['after_attach'];
                }
               
$Debuglog->add( $log_message, array( 'error', 'files' ) );
                continue;
            }

           
$params['File'] = $File;
           
$params['Item'] = $this;

           
$temp_params = $params;
            foreach(
$params as $param_key => $param_value )
            {
// Pass all params by reference, in order to give possibility to modify them by plugin
                // So plugins can add some data before/after image tags (E.g. used by infodots plugin)
               
$params[ $param_key ] = & $params[ $param_key ];
            }

           
// Prepare params before rendering item attachment:
           
$Plugins->trigger_event_first_true_with_params( 'PrepareForRenderItemAttachment', $params );

            if(
count( $Plugins->trigger_event_first_true( 'RenderItemAttachment', $params ) ) != 0 )
            {    
// This attachment has been rendered by a plugin (to $params['data']), Skip this from core rendering:
               
continue;
            }

            if( ! isset(
$params['image_attachment'] ) && $File->is_image() )
            {
// Skip images (except those in the attachment position) because these are displayed inline already
                // fp> TODO: have a setting for each linked file to decide whether it should be displayed inline or as an attachment
               
continue;
            }
            elseif(
$File->is_dir() )
            {
// Skip directories/galleries
               
continue;
            }

           
// A link to download a file:

            // Just icon with download icon:
           
$icon = ( $params['display_download_icon'] && $File->exists() && strpos( $params['attach_format'].$params['file_link_format'], '$icon$' ) !== false ) ?
                   
get_icon( $params['download_link_icon'], 'imgtag', array( 'title' => $params['download_link_title'] ) ) : '';

           
// A link with icon to download:
           
$icon_link = ( $params['display_download_icon'] && $File->exists() && strpos( $params['attach_format'], '$icon_link$' ) !== false ) ?
                   
action_icon( $params['download_link_title'], $params['download_link_icon'], $Link->get_download_url(), '', 5 ) : '';

           
// File size info:
           
$file_size = ( $params['display_file_size'] && $File->exists() && strpos( $params['attach_format'].$params['file_link_format'], '$file_size$' ) !== false ) ?
                   
$params['before_attach_size'].bytesreadable( $File->get_size(), false, false ).$params['after_attach_size'] : '';

           
// File description:
           
$file_desc = '';
            if(
$params['display_file_desc'] && $File->exists() && strpos( $params['attach_format'].$params['file_link_format'], '$file_desc$' ) !== false )
            {    
// If description should be displayed:
               
$file_desc = nl2br( trim( $File->get( 'desc' ) ) );
                if(
$file_desc !== '' )
                {    
// If file has a filled description:
                   
$params['before_file_desc'].$file_desc.$params['after_file_desc'];
                }
            }

           
// A link with file name or file title to download:
           
$file_link_format = str_replace( array( '$icon$', '$file_name$', '$file_size$' ),
                array(
$icon, '$text$', $file_size ),
               
$params['file_link_format'] );
            if(
$params['file_link_text'] == 'filename' || trim( $File->get( 'title' ) ) === '' )
            {    
// Use file name for link text:
               
$file_link_text = $File->get_name();
            }
            else
            {    
// Use file title only if it filled:
               
$file_link_text = $File->get( 'title' );
            }
            if(
$File->exists() )
            {    
// Get file link to download if file exists:
               
$file_download_url = $this->get_coll_setting( 'download_enable' ) ? $Link->get_download_url() : NULL;
               
$file_link = ( strpos( $params['attach_format'], '$file_link$' ) !== false ) ?
                       
$File->get_view_link( $file_link_text, NULL, NULL, $file_link_format, $params['file_link_class'], $file_download_url ) : '';
            }
            else
            {    
// File doesn't exist, We cannot display a link, Display only file name and warning:
               
$file_link = ( strpos( $params['attach_format'], '$file_link$' ) !== false ) ?
                       
$file_link_text.' - <span class="red nowrap">'.get_icon( 'warning_yellow' ).' '.T_('Missing attachment!').'</span>' : '';
            }

           
$r_file[$i] = $params['before_attach'];
           
$r_file[$i] .= str_replace( array( '$icon$', '$icon_link$', '$file_link$', '$file_size$', '$file_desc$' ),
                array(
$icon, $icon_link, $file_link, $file_size, $file_desc ),
               
$params['attach_format'] );
           
$r_file[$i] .= $params['after_attach'];

           
$i++;
           
$params = $temp_params;
        }

        if( !empty(
$r_file) )
        {
           
$r = $params['before'].implode( "\n", $r_file ).$params['after'];

           
// Character conversions
           
$r = format_to_output( $r, $format );
        }

        return
$r;
    }


   
/**
     * Get array of the Files that are used as "Fallback" for the selected File
     *
     * @param object File
     * @return array Fallback Files
     */
   
function get_fallback_files( $File )
    {
       
$fallback_files = array();

        if( empty(
$File ) )
        {
// No File for fallbacks
           
return $fallback_files;
        }

        if( ! isset(
$this->fallback_FileList ) )
        {
// Get list of attached fallback files
           
$LinkOwner = new LinkItem( $this );
            if( !
$this->fallback_FileList = $LinkOwner->get_attachment_FileList( 1000, 'fallback' ) )
            {
// No fallback files
               
return $fallback_files;
            }
        }

       
// Get file name without extension
       
$file_name_without_ext = preg_replace( '#^(.+)\.[^\.]+$#', '$1', $File->get_name() );

       
// Rewind internal index to first position
       
$this->fallback_FileList->current_idx = 0;

        while(
$fallback_File = & $this->fallback_FileList->get_next() )
        {
            if(
$File->get_name() != $fallback_File->get_name() &&
               
preg_match( '#^'.$file_name_without_ext.'\.[^\.]+$#', $fallback_File->get_name() ) )
            {
// Fallback is a file with same name but with different extension
               
$fallback_files[] = $fallback_File;
            }
        }

        return
$fallback_files;
    }


   
/**
     * Get placeholder image File that has the same name as the current video File
     *
     * @param object File
     * @return object placeholder File
     */
   
function & get_placeholder_File( $video_File )
    {
       
$r = NULL;

        if( empty(
$video_File ) )
        {
// No File for placeholder
           
return $r;
        }

        if( ! isset(
$this->placeholder_FileList ) )
        {
// Get list of attached fallback files
           
$LinkOwner = new LinkItem( $this );
           
$attachment_FileList = $LinkOwner->get_attachment_FileList( 1000 );
            if( !
$this->placeholder_FileList = & $attachment_FileList )
            {
// No attached files
               
return $r;
            }
        }

       
// Get file name without extension
       
$video_file_name_without_ext = preg_replace( '#^(.+)\.[^\.]+$#', '$1', $video_File->get_name() );

       
// Rewind internal index to first position
       
$this->placeholder_FileList->current_idx = 0;

        while(
$attached_File = & $this->placeholder_FileList->get_next() )
        {
            if(
$video_File->get_name() != $attached_File->get_name() &&
               
preg_match( '#^'.$video_file_name_without_ext.'\.(jpg|jpeg|png|gif)+$#', $attached_File->get_name() ) )
            {
// It is a file with same name but with image extension
               
return $attached_File;
            }
        }

        return
$r;
    }


   
/**
     * @param array Associative array of parameters
     * @return string Output
     */
   
function attachment_files( & $params/* = array()*/ )
    {
        global
$Plugins;

       
$r = '';

       
$ItemAttachment_plugins = $Plugins->get_list_by_event( 'RenderItemAttachment' );

       
$params['Item'] = $this;

       
$temp_params = $params;
        foreach(
$params as $param_key => $param_value )
        {
// Pass all params by reference, in order to give possibility to modify them by plugin
            // So plugins can add some data before/after image tags (E.g. used by infodots plugin)
           
$params[ $param_key ] = & $params[ $param_key ];
        }

       
$Plugins->trigger_event_first_true( 'RenderItemAttachment', $params );

       
$params = $temp_params;

        return
$r;
    }


   
/**
     * Template function: Displays link to the feed for comments on this item
     *
     * @param string Type of feedback to link to (rss2/atom)
     * @param string String to display before the link (if comments are to be displayed)
     * @param string String to display after the link (if comments are to be displayed)
     * @param string Link title
     */
   
function feedback_feed_link( $skin = '_rss2', $before = '', $after = '', $title='#' )
    {
        if( !
$this->can_see_comments() )
        {    
// Comments disabled
           
return;
        }

       
$this->load_Blog();

        if(
$this->Blog->get_setting( 'comment_feed_content' ) == 'none' )
        {    
// Comment feeds disabled
           
return;
        }

        if( ! (
$url = $this->get_feedback_feed_url( $skin ) ) )
        {    
// Don't display feed link when no feed skin is installed in system:
           
return;
        }

        if(
$title == '#' )
        {
           
$title = get_icon( 'feed' ).' '.T_('Comment feed for this post');
        }

        echo
$before;
        echo
'<a href="'.$url.'">'.format_to_output($title).'</a>';
        echo
$after;
    }


   
/**
     * Get URL to display the post comments in an XML feed.
     *
     * @param string Skin folder name
     * @return string|false URL or FALSE if none feed skin is not installed in system
     */
   
function get_feedback_feed_url( $skin_folder_name )
    {
       
$item_Blog = & $this->get_Blog();
       
$comment_feed_url = $item_Blog->get_comment_feed_url( $skin_folder_name );
        return (
$comment_feed_url ? url_add_param( $comment_feed_url, 'p='.$this->ID ) : false );
    }


   
/**
     * Get URL to display the post comments.
     *
     * @return string
     */
   
function get_feedback_url( $popup = false, $glue = '&amp;', $blog_ID = NULL )
    {
        if(
$blog_ID !== NULL &&
                (
$BlogCache = & get_BlogCache() ) &&
                (
$Blog = & $BlogCache->get_by_ID( $blog_ID, false, false ) ) )
        {
           
$blog_url = $Blog->get( 'url' );
        }
        else
        {
           
$blog_url = '';
        }

       
$url = $this->get_single_url( 'auto', $blog_url, $glue, $blog_ID );
        if(
$popup )
        {
           
$url = url_add_param( $url, 'disp=feedback-popup', $glue );
        }

        return
$url;
    }


   
/**
     * Template function: Displays link to feedback page (under some conditions)
     *
     * @param array
     */
   
function feedback_link( $params )
    {
        echo
$this->get_feedback_link( $params );
    }


   
/**
     * Get a link to feedback page (under some conditions)
     *
     * @param array
     */
   
function get_feedback_link( $params = array() )
    {
        global
$ReqURL, $Blog, $Settings;

        if( !
$this->can_see_comments() )
        {    
// Comments disabled
           
return;
        }

       
$params = array_merge( array(
               
'type' => 'feedbacks',        // Kind of feedbacks to count
               
'status' => '#',    // Statuses of feedbacks to count, can be a string for one status or array for several. '#' - active front-office comment statuses, '#moderation#' - "require moderation" statuses.
               
'link_before' => '',
               
'link_after' => '',
               
'link_text_zero' => '#',
               
'link_text_one' => '#',
               
'link_text_more' => '#',
               
'link_anchor_zero' => '#',
               
'link_anchor_one' => '#',
               
'link_anchor_more' => '#',
               
'link_title' => '#',
               
'link_class' => '',
               
'show_in_single_mode' => false,        // Do we want to show this link even if we are viewing the current post in single view mode
               
'url' => '#',
               
'stay_in_same_collection' => 'auto', // 'auto' - follow 'allow_crosspost_urls' if we are cross posted, true - always stay in same collection if we are cross posted, false - always go to permalink if we are cross posted
           
), $params );

        if(
$params['show_in_single_mode'] == false && is_single_page( $this->ID ) )
        {    
// We are viewing the single page for this Item, which (typically) contains comments, so we don't want to display this link
           
return;
        }

        if( isset(
$Blog ) &&
            (
$params['stay_in_same_collection'] === true || // always stay in current collection
             
( $params['stay_in_same_collection'] == 'auto' && ( $item_Blog = & $this->get_Blog() ) && $item_Blog->get_setting( 'allow_crosspost_urls' ) ) // follow 'allow_crosspost_urls' to stay in current collection
           
) )
        {    
// Use current collection if this Item is cross posted and has at least one category from current collection:
           
$current_blog_ID = $Blog->ID;
        }
        else
        {    
// Use main collection of this Item:
           
$current_blog_ID = NULL;
        }

       
// dh> TODO:    Add plugin hook, where a Pingback plugin could hook and provide "pingbacks"
       
switch( $params['type'] )
        {
            case
'feedbacks':
                if(
$params['link_title'] == '#' ) $params['link_title'] = T_('Display feedback / Leave a comment');
                if(
$params['link_text_zero'] == '#' ) $params['link_text_zero'] = T_('Send feedback').' &raquo;';
                if(
$params['link_text_one'] == '#' ) $params['link_text_one'] = T_('1 feedback').' &raquo;';
                if(
$params['link_text_more'] == '#' ) $params['link_text_more'] = T_('%d feedbacks').' &raquo;';
                break;

            case
'comments':
                if(
$params['link_title'] == '#' ) $params['link_title'] = T_('Display comments / Leave a comment');
                if(
$params['link_text_zero'] == '#' )
                {
                    if(
$this->can_comment( NULL ) ) // NULL, because we do not want to display errors here!
                   
{
                       
$params['link_text_zero'] = T_('Leave a comment').' &raquo;';
                    }
                    else
                    {
                       
$params['link_text_zero'] = '';
                    }
                }
                if(
$params['link_text_one'] == '#' ) $params['link_text_one'] = T_('1 comment').' &raquo;';
                if(
$params['link_text_more'] == '#' ) $params['link_text_more'] = T_('%d comments').' &raquo;';
                break;

            case
'trackbacks':
               
$this->get_Blog();
                if( !
$this->can_receive_pings() )
                {
// Trackbacks not allowed on this blog:
                   
return;
                }
                if(
$params['link_title'] == '#' ) $params['link_title'] = T_('Display trackbacks / Get trackback address for this post');
                if(
$params['link_text_zero'] == '#' ) $params['link_text_zero'] = T_('Send a trackback').' &raquo;';
                if(
$params['link_text_one'] == '#' ) $params['link_text_one'] = T_('1 trackback').' &raquo;';
                if(
$params['link_text_more'] == '#' ) $params['link_text_more'] = T_('%d trackbacks').' &raquo;';
                break;

            case
'pingbacks':
               
// Obsolete, but left for skin compatibility
               
$this->get_Blog();
                if( !
$this->can_receive_pings() )
                {
// Trackbacks not allowed on this blog:
                    // We'll consider pingbacks to follow the same restriction
                   
return;
                }
                if(
$params['link_title'] == '#' ) $params['link_title'] = T_('Display pingbacks');
                if(
$params['link_text_zero'] == '#' ) $params['link_text_zero'] = T_('No pingback yet').' &raquo;';
                if(
$params['link_text_one'] == '#' ) $params['link_text_one'] = T_('1 pingback').' &raquo;';
                if(
$params['link_text_more'] == '#' ) $params['link_text_more'] = T_('%d pingbacks').' &raquo;';
                break;

            case
'webmentions':
                if( !
$this->can_receive_webmentions() )
                {
// Webmentions not allowed on this collection:
                    // We'll consider webmentions to follow the same restriction
                   
return;
                }
                if(
$params['link_title'] == '#' ) $params['link_title'] = T_('Display webmentions');
                if(
$params['link_text_zero'] == '#' ) $params['link_text_zero'] = T_('No webmention yet').' &raquo;';
                if(
$params['link_text_one'] == '#' ) $params['link_text_one'] = T_('1 webmention').' &raquo;';
                if(
$params['link_text_more'] == '#' ) $params['link_text_more'] = T_('%d webmentions').' &raquo;';
                break;

            default:
               
debug_die( "Unknown feedback type [{$params['type']}]" );
        }

       
$link_text = $this->get_feedback_title( $params['type'], $params['link_text_zero'], $params['link_text_one'], $params['link_text_more'], $params['status'] );

        if( empty(
$link_text) )
        {    
// No link, no display...
           
return false;
        }

        if(
$params['url'] == '#' )
        {
// We want a link to single post:
           
$params['url'] = $this->get_feedback_url( false, '&amp;', $current_blog_ID );
        }

       
// Anchor position
       
$number = generic_ctp_number( $this->ID, $params['type'], $params['status'] );

        if(
$number == 0 )
           
$anchor = $params['link_anchor_zero'];
        elseif(
$number == 1 )
           
$anchor = $params['link_anchor_one'];
        elseif(
$number > 1 )
           
$anchor = $params['link_anchor_more'];
        if(
$anchor == '#' )
        {
           
$anchor = '#'.$params['type'];
        }

       
$r = $params['link_before'];

        if( !empty(
$params['url'] ) )
        {
           
$r .= '<a href="'.$params['url'].$anchor.'" class="'.format_to_output( $params['link_class'], 'htmlattr' ).'" ';    // Position on feedback
           
$r .= 'title="'.format_to_output( $params['link_title'], 'htmlattr' ).'"';
           
$r .= '>';
           
$r .= $link_text;
           
$r .= '</a>';
        }
        else
        {
           
$r .= $link_text;
        }

       
$r .= $params['link_after'];

        return
$r;
    }


   
/**
     * Return true if there is any feedback of given type.
     *
     * @param array
     * @return boolean
     */
   
function has_feedback( $params )
    {
       
$params = array_merge( array(
                           
'type' => 'feedbacks',
                           
'status' => 'published'
                       
), $params );

       
// Check is a given type is allowed
       
switch( $params['type'] )
        {
            case
'feedbacks':
            case
'comments':
            case
'trackbacks':
            case
'pingbacks':
            case
'webmentions':
                break;
            default:
               
debug_die( "Unknown feedback type [{$params['type']}]" );
        }

       
$number = generic_ctp_number( $this->ID, $params['type'], $params['status'] );

        return
$number > 0;
    }


   
/**
     * Return true if trackbacks and pingbacks are allowed
     *
     * @return boolean
     */
   
function can_receive_pings()
    {
       
$this->load_Blog();
        return
$this->Blog->get( 'allowtrackbacks' ) && $this->can_comment( NULL );
    }


   
/**
     * Return true if webmentions are allowed
     *
     * @return boolean
     */
   
function can_receive_webmentions()
    {
       
$this->load_Blog();
        return
$this->Blog->get_setting( 'webmentions' ) && $this->can_comment( NULL );
    }


   
/**
     * Get text depending on number of comments
     *
     * @param string Type of feedback to link to (feedbacks (all)/comments/trackbacks/pingbacks/webmentions)
     * @param string Link text to display when there are 0 comments
     * @param string Link text to display when there is 1 comment
     * @param string Link text to display when there are >1 comments (include %d for # of comments)
     * @param string|array Statuses of feedbacks to count, a string for one status, an array for several statuses,
     *                     '#' - to use currently active front-office comment statuses of the item's collection
     *                     '#moderation#' - to use all comment statuses which require moderation on front-office for the item's collection
     */
   
function get_feedback_title( $type = 'feedbacks',    $zero = '#', $one = '#', $more = '#', $statuses = '#', $filter_by_perm = true )
    {
        if( !
$this->can_see_comments() )
        {    
// Comments disabled
           
return NULL;
        }

       
// dh> TODO:    Add plugin hook, where a Pingback plugin could hook and provide "pingbacks"
       
switch( $type )
        {
            case
'feedbacks':
                if(
$zero == '#' ) $zero = '';
                if(
$one == '#' ) $one = T_('1 feedback');
                if(
$more == '#' ) $more = T_('%d feedbacks');
                break;

            case
'comments':
                if(
$zero == '#' ) $zero = '';
                if(
$one == '#' ) $one = T_('1 comment');
                if(
$more == '#' ) $more = T_('%d comments');
                break;

            case
'trackbacks':
                if(
$zero == '#' ) $zero = '';
                if(
$one == '#' ) $one = T_('1 trackback');
                if(
$more == '#' ) $more = T_('%d trackbacks');
                break;

            case
'pingbacks':
               
// Obsolete, but left for skin compatibility
               
if( $zero == '#' ) $zero = '';
                if(
$one == '#' ) $one = T_('1 pingback');
                if(
$more == '#' ) $more = T_('%d pingbacks');
                break;

            case
'metas':
                if(
$zero == '#' ) $zero = '';
                if(
$one == '#' ) $one = T_('1 internal comment');
                if(
$more == '#' ) $more = T_('%d internal comments');
                break;

            case
'webmentions':
                if(
$zero == '#' ) $zero = '';
                if(
$one == '#' ) $one = T_('1 webmention');
                if(
$more == '#' ) $more = T_('%d webmentions');
                break;

            default:
               
debug_die( "Unknown feedback type [$type]" );
        }

        if(
$statuses == '#' )
        {    
// Get all comment statuses which are actived on front-office for the item's collection:
           
$this->load_Blog();
           
$statuses = explode( ',', $this->Blog->get_setting( 'comment_inskin_statuses' ) );
        }
        elseif(
$statuses == '#moderation#' )
        {    
// Get all comment statuses which require moderation on front-office for the item's collection:
           
$this->load_Blog();
           
$statuses = explode( ',', $this->Blog->get_setting( 'moderation_statuses' ) );
        }

       
$number = generic_ctp_number( $this->ID, $type, $statuses, false, $filter_by_perm );
        if( !
$filter_by_perm )
        {
// This is the case when we are only counting comments awaiting moderation, return only not visible feedbacks number
            // count feedbacks with the same statuses where user has permission
           
$visible_number = generic_ctp_number( $this->ID, $type, $statuses, false, true );
           
$number = $number - $visible_number;
        }

        if(
$number == 0 )
            return
$zero;
        elseif(
$number == 1 )
            return
$one;
        elseif(
$number > 1 )
            return
str_replace( '%d', $number, $more );
    }


   
/**
     * Get table from ratings data
     *
     * @param array ratings data
     * @param array params
     */
   
function get_rating_table( $ratings, $params )
    {
       
$ratings_count = $ratings['all_ratings'];
       
$average_real = ( $ratings_count > 0 ) ? number_format( $ratings["summary"] / $ratings_count, 1, ".", "" ) : 0;
       
$average = ceil( ( $average_real ) / 5 * 100 );

       
$table = '<table class="rating_summary" cellspacing="1">';
        foreach (
$ratings as $r => $count )
        {    
// Print a row for each star with formed data
           
if( !is_int($r) )
            {
                continue;
            }

           
$star_average = ( $ratings_count > 0 ) ? ceil( ( $count / $ratings_count ) * 100 ) : 0;
            switch(
$params['rating_summary_star_totals'] )
            {
                case
'count':
                   
$star_value = '('.$count.')';
                break;
                case
'percent':
                   
$star_value = '('.$star_average.'%)';
                break;
                case
'none':
                default:
                   
$star_value = "";
                break;
            }
           
$table .= '<tr><th>'.$r.' '.T_('star').':</th>
                <td class="progress"><div style="width:'
.$star_average.'%">&nbsp;</div></td>
                <td>'
.$star_value.'</td><tr>';
        }
       
$table .= '</table>';

       
$table .= '<div class="rating_summary_total">
            '
.$ratings_count.' '.( $ratings_count > 1 ? T_('ratings') : T_('rating') ).'
            <div class="average_rating">'
.T_('Average user rating').':<br />
            '
.get_star_rating( $average_real ).'<span class="average_rating_score">('.$average_real.')</span>
            </div></div><div class="clear"></div>'
;

        return
$table;
    }


   
/**
     * Get table with rating summary
     *
     * @param array of params
     */
   
function get_rating_summary( $params = array() )
    {
       
// Make sure we are not missing any param:
       
$params = array_merge( array(
           
'rating_summary_star_totals' => 'count' // Possible values: 'count', 'percent' and 'none'
       
), $params );

       
$item_Blog = & $this->get_Blog();
        if( !
$item_Blog->get_setting( 'display_rating_summary' ) )
        {
// Don't display a rating summary
           
return;
        }

       
// get ratings and active ratings ( active ratings are younger then comment_expiry_delay )
       
list( $ratings, $active_ratings ) = $this->get_ratings();
       
$ratings_count = $ratings['all_ratings'];
       
$active_ratings_count = $active_ratings['all_ratings'];
        if(
$ratings_count == 0 )
        {
// No Comments
           
return;
        }

       
$average_real = number_format( $ratings["summary"] / $ratings_count, 1, ".", "" );
       
$active_average_real = ( $active_ratings_count == 0 ) ? 0 : ( number_format( $active_ratings["summary"] / $active_ratings_count, 1, ".", "" ) );

       
$result = '';
       
$expiry_delay = $this->get_setting( 'comment_expiry_delay' );
        if( empty(
$expiry_delay ) )
        {
           
$all_ratings_title = T_('User ratings');
        }
        else
        {
           
$all_ratings_title = T_('Overall user ratings');
           
$result .= '<div class="ratings_table">';
           
$result .= '<div><strong>'.get_duration_title( $expiry_delay ).'</strong></div>';
           
$result .= $this->get_rating_table( $active_ratings, $params );
           
$result .= '</div>';
        }

       
$result .= '<div class="ratings_table">';
       
$result .= '<div><strong>'.$all_ratings_title.'</strong></div>';
       
$result .= $this->get_rating_table( $ratings, $params );
       
$result .= '</div>';

        return
$result;
    }


   
/**
     * Template function: Displays feeback moderation info
     *
     * @param string Type of feedback to link to (feedbacks (all)/comments/trackbacks/pingbacks/webmentions)
     * @param string String to display before the link (if comments are to be displayed)
     * @param string String to display after the link (if comments are to be displayed)
     * @param string Link text to display when there are 0 comments
     * @param string Link text to display when there is 1 comment
     * @param string Link text to display when there are >1 comments (include %d for # of comments)
     * @param string Link
     * @param boolean true to hide if no feedback
     */
   
function feedback_moderation( $type = 'feedbacks', $before = '', $after = '',
           
$zero = '', $one = '#', $more = '#', $edit_comments_link = '#', $params = array() )
    {
       
/**
         * @var User
         */
       
global $current_User;

       
/* TODO: finish this...
        $params = array_merge( array(
                                    'type' => 'feedbacks',
                                    'block_before' => '',
                                    'blo_after' => '',
                                    'link_text_zero' => '#',
                                    'link_text_one' => '#',
                                    'link_text_more' => '#',
                                    'link_title' => '#',
                                    'url' => '#',
                                    'type' => 'feedbacks',
                                ), $params );
        */

       
if( isset($current_User) &&    check_user_perm( 'blog_comment!draft', 'moderate', false, $this->get_blog_ID() ) )
        {    
// We have permission to edit comments:
           
if( $edit_comments_link == '#' )
            {    
// Use default link:
               
global $admin_url;
               
$edit_comments_link = '<a href="'.$admin_url.'?ctrl=items&amp;blog='.$this->get_blog_ID().'&amp;p='.$this->ID.'#comments" title="'.T_('Moderate these feedbacks').'">'.get_icon( 'edit' ).' '.T_('Moderate').'...</a>';
            }
        }
        else
        {
// User has no right to edit comments:
           
$edit_comments_link = '';
        }

       
// Inject Edit/moderate link as relevant:
       
$zero = str_replace( '%s', $edit_comments_link, $zero );
       
$one = str_replace( '%s', $edit_comments_link, $one );
       
$more = str_replace( '%s', $edit_comments_link, $more );

       
$r = $this->get_feedback_title( $type, $zero, $one, $more, '#moderation#', false );

        if( !empty(
$r ) )
        {
            echo
$before.$r.$after;
        }
    }



   
/**
     * Template tag: display footer for the current Item.
     *
     * @param array
     * @return boolean true if something has been displayed
     */
   
function footer( $params )
    {
       
// Make sure we are not missing any param:
       
$params = array_merge( array(
               
'mode'        => '#',                // Will detect 'single' from $disp automatically
               
'block_start' => '<div class="item_footer">',
               
'block_end'   => '</div>',
               
'format'      => 'htmlbody',
            ),
$params );

        if(
$params['mode'] == '#' )
        {
            global
$disp;
           
$params['mode'] = $disp;
        }

       
// pre_dump( $params['mode'] );

       
$this->get_Blog();
        switch(
$params['mode'] )
        {
            case
'xml':
               
$text = $this->Blog->get_setting( 'xml_item_footer_text' );
                break;

            case
'single':
               
$text = $this->Blog->get_setting( 'single_item_footer_text' );
                break;

            default:
               
// Do NOT display!
               
$text = '';
        }

       
$text = preg_replace_callback( '#\$([a-z_]+)\$#', array( $this, 'replace_callback' ), $text );

        if( empty(
$text) )
        {
            return
false;
        }

        echo
format_to_output( $params['block_start'].$text.$params['block_end'], $params['format'] );

        return
true;
    }


   
/**
     * Gets button for deleting the Item if user has proper rights
     *
     * @param string to display before link
     * @param string to display after link
     * @param string link text
     * @param string link title
     * @param string class name
     * @param boolean true to make this a button instead of a link
     * @param string page url for the delete action
     * @param string confirmation text
     */
   
function get_delete_link( $before = ' ', $after = ' ', $text = '#', $title = '#', $class = '', $button = false, $actionurl = '#', $confirm_text = '#', $redirect_to = '' )
    {
        global
$admin_url;

        if( !
check_user_perm( 'item_post!CURSTATUS', 'delete', false, $this, false ) )
        {    
// User has no rights to delete this Item:
           
return false;
        }

        if(
$text == '#' )
        {
            if( !
$button )
            {
               
$text = get_icon( 'delete', 'imgtag' ).' '.T_('Delete!');
            }
            else
            {
               
$text = T_('Delete!');
            }
        }

        if(
$title == '#' ) $title = T_('Delete this post');

        if(
$actionurl == '#' )
        {
           
$actionurl = $admin_url.'?ctrl=items&amp;action=delete&amp;post_ID=';
        }
       
$url = $actionurl.$this->ID.'&amp;'.url_crumb('item');

        if( !empty(
$redirect_to ) )
        {
           
$url = $url.'&amp;redirect_to='.rawurlencode( $redirect_to );
        }

        if(
$confirm_text == '#' )
        {
           
$confirm_text = TS_('You are about to delete this post!\\nThis cannot be undone!');
        }

       
$r = $before;
        if(
$button )
        {
// Display as button
           
$r .= '<input type="button"';
           
$r .= ' value="'.$text.'" title="'.$title.'" onclick="if ( confirm(\'';
           
$r .= $confirm_text;
           
$r .= '\') ) { document.location.href=\''.$url.'\' }"';
            if( !empty(
$class ) ) $r .= ' class="'.$class.'"';
           
$r .= '/>';
        }
        else
        {
// Display as link
           
$r .= '<a href="'.$url.'" title="'.$title.'" onclick="return confirm(\'';
           
$r .= $confirm_text;
           
$r .= '\')"';
            if( !empty(
$class ) ) $r .= ' class="'.$class.'"';
           
$r .= '>'.$text.'</a>';
        }
       
$r .= $after;

        return
$r;
    }


   
/**
     * Displays button for deleting the Item if user has proper rights
     *
     * @param string to display before link
     * @param string to display after link
     * @param string link text
     * @param string link title
     * @param string class name
     * @param boolean true to make this a button instead of a link
     * @param string page url for the delete action
     * @param string confirmation text
     */
   
function delete_link( $before = ' ', $after = ' ', $text = '#', $title = '#', $class = '', $button = false, $actionurl = '#', $confirm_text = '#', $redirect_to = '' )
    {
        echo
$this->get_delete_link( $before, $after, $text, $title, $class, $button, $actionurl, $confirm_text, $redirect_to );
    }


   
/**
     * Provide link to copy a post if user has edit rights
     *
     * @param array Params:
     *  - 'before': to display before link
     *  - 'after':    to display after link
     *  - 'text': link text
     *  - 'title': link title
     *  - 'class': CSS class name
     *  - 'save_context': redirect to current URL?
     */
   
function get_copy_link( $params = array() )
    {
        global
$admin_url;

       
$actionurl = $this->get_copy_url($params);
        if( !
$actionurl )
        {
            return
false;
        }

       
// Make sure we are not missing any param:
       
$params = array_merge( array(
               
'before'       => ' ',
               
'after'        => ' ',
               
'text'         => '#', // '#' - icon + text, '#icon#' - only icon, '#text#' - only text
               
'title'        => '#',
               
'class'        => '',
               
'save_context' => true,
            ),
$params );

        switch(
$params['text'] )
        {
            case
'#':
               
$params['text'] = get_icon( 'copy', 'imgtag', array( 'title' => T_('Duplicate this post...') ) ).' '.T_('Duplicate...');
                break;

            case
'#icon#':
               
$params['text'] = get_icon( 'copy', 'imgtag', array( 'title' => T_('Duplicate this post...') ) );
                break;

            case
'#text#':
               
$params['text'] = T_('Duplicate...');
                break;
        }

        if(
$params['title'] == '#' ) $params['title'] = T_('Duplicate this post...');

       
$r = $params['before'];
       
$r .= '<a href="'.$actionurl;
       
$r .= '" title="'.$params['title'].'"';
        if( !empty(
$params['class'] ) ) $r .= ' class="'.$params['class'].'"';
       
$r .=  '>'.$params['text'].'</a>';
       
$r .= $params['after'];

        return
$r;
    }


   
/**
     * Get URL to copy a post if user has edit rights.
     *
     * @param array Params:
     *  - 'save_context': redirect to current URL?
     */
   
function get_copy_url( $params = array() )
    {
        global
$admin_url;

        if( !
is_logged_in( false ) ) return false;

        if( !
$this->ID )
        {
// preview..
           
return false;
        }

       
$this->load_Blog();
       
$write_item_url = $this->Blog->get_write_item_url();
        if( empty(
$write_item_url ) )
        {
// User has no right to copy this post
           
return false;
        }

       
// default params
       
$params += array('save_context' => true);

       
$url = false;
        if(
$this->Blog->get_setting( 'in_skin_editing' ) && !is_admin_page() )
        {    
// We have a mode 'In-skin editing' for the current Blog
           
if( check_item_perm_edit( 0, false ) )
            {    
// Current user can copy this post from Front-office
               
$url = url_add_param( $this->Blog->get( 'url' ), 'disp=edit&cp='.$this->ID );
            }
            else if(
check_user_perm( 'admin', 'restricted' ) )
            {    
// Current user can copy this post from Back-office
               
$url = $admin_url.'?ctrl=items&amp;action=copy&amp;blog='.$this->Blog->ID.'&amp;p='.$this->ID;
            }
        }
        else if(
check_user_perm( 'admin', 'restricted' ) )
        {    
// Copy a post from Back-office
           
$url = $admin_url.'?ctrl=items&amp;action=copy&amp;blog='.$this->Blog->ID.'&amp;p='.$this->ID;
            if(
$params['save_context'] )
            {
               
$url .= '&amp;redirect_to='.rawurlencode( regenerate_url( '', '', '', '&' ).'#'.$this->get_anchor_id() );
            }
        }
        return
$url;
    }


   
/**
     * Template tag
     * @see Item::get_copy_link()
     */
   
function copy_link( $params = array() )
    {
        echo
$this->get_copy_link( $params );
    }


   
/**
     * Provide a link to add a new version of this post if user has rights
     *
     * @param array Params:
     *  - 'before': to display before link
     *  - 'after':    to display after link
     *  - 'text': link text
     *  - 'title': link title
     *  - 'class': CSS class name
     * @return string
     */
   
function get_add_version_link( $params = array() )
    {
        if( !
$this->can_link_version( true ) )
        {    
// New item version cannot be added by some restriction:
           
return false;
        }

       
// Make sure we are not missing any param:
       
$params = array_merge( array(
               
'before'       => '',
               
'after'        => '',
               
'text'         => '#text#', // '#' - icon + text, '#icon#' - only icon, '#text#' - only text
               
'title'        => '#', // '#' - Add version...
               
'class'        => '',
            ),
$params );

        switch(
$params['text'] )
        {
            case
'#text#':
               
$params['text'] = T_('Add version').'...';
                break;

            case
'#':
               
$params['text'] = get_icon( 'add', 'imgtag', array( 'title' => T_('Add version').'...' ) ).' '.T_('Add version').'...';
                break;

            case
'#icon#':
               
$params['text'] = get_icon( 'add', 'imgtag', array( 'title' => T_('Add version').'...' ) );
                break;
        }

        if(
$params['title'] == '#' )
        {
           
$params['title'] = T_('Add version').'...';
        }

       
$r = $params['before'];

       
$r .= '<a href="#" onclick="return evo_add_version_load_window( '.$this->ID.' )"'
               
.'title="'.format_to_output( $params['title'], 'htmlattr' ).'"'
               
.( empty( $params['class'] ) ? '' : ' class="'.$params['class'].'"' )
            .
'>'.format_to_output( $params['text'], 'htmlbody' ).'</a>';

       
$r .= $params['after'];

        return
$r;
    }


   
/**
     * Provide a link to link a new version of this post if user has rights
     *
     * @param array Params:
     *  - 'before': to display before link
     *  - 'after':    to display after link
     *  - 'text': link text
     *  - 'title': link title
     *  - 'class': CSS class name
     * @return string
     */
   
function get_link_version_link( $params = array() )
    {
        if( !
$this->can_link_version(  true  ) )
        {    
// New item version cannot be linked by some restriction:
           
return false;
        }

       
// Make sure we are not missing any param:
       
$params = array_merge( array(
               
'before'       => '',
               
'after'        => '',
               
'text'         => '#text#', // '#' - icon + text, '#icon#' - only icon, '#text#' - only text
               
'title'        => '#', // '#' - Link version...
               
'class'        => '',
            ),
$params );

        switch(
$params['text'] )
        {
            case
'#text#':
               
$params['text'] = T_('Link version').'...';
                break;

            case
'#':
               
$params['text'] = get_icon( 'link', 'imgtag', array( 'title' => T_('Link version').'...' ) ).' '.T_('Link version').'...';
                break;

            case
'#icon#':
               
$params['text'] = get_icon( 'link', 'imgtag', array( 'title' => T_('Link version').'...' ) );
                break;
        }

        if(
$params['title'] == '#' )
        {
           
$params['title'] = T_('Link version').'...';
        }

       
$r = $params['before'];

       
$item_Blog = & $this->get_Blog();
       
$r .= '<a href="#" onclick="return evo_link_version_load_window( '.$this->ID.', \''.$item_Blog->get( 'urlname' ).'\' )"'
               
.'title="'.format_to_output( $params['title'], 'htmlattr' ).'"'
               
.( empty( $params['class'] ) ? '' : ' class="'.$params['class'].'"' )
            .
'>'.format_to_output( $params['text'], 'htmlbody' ).'</a>';

       
$r .= $params['after'];

        return
$r;
    }


   
/**
     * Get URL to unlink a post if user has a permission
     *
     * @param array Params:
     *  - 'unlink_item_ID': What item to unlink, NULL - to unlink this Item
     */
   
function get_unlink_version_url( $params = array() )
    {
        global
$admin_url;

        if( !
$this->can_link_version() )
        {    
// Item version cannot be unlinked by some restriction:
           
return false;
        }

       
// Default params:
       
$params = array_merge( array(
               
'unlink_item_ID' => NULL,
            ),
$params );

        if(
$params['unlink_item_ID'] !== NULL )
        {
           
$ItemCache = & get_ItemCache();
            if( ! (
$unlink_item = & $ItemCache->get_by_ID( $params['unlink_item_ID'], false, false ) ) ||
               
$unlink_item->get( 'igrp_ID' ) != $this->get( 'igrp_ID' ) )
            {    
// If the requested Item to unlink is from different group:
               
return false;
            }
        }

        return
$admin_url.'?ctrl=items&amp;action=unlink_version&amp;blog='.$this->Blog->ID
           
.'&amp;post_ID='.$this->ID
           
.( empty( $unlink_item ) ? '' : '&amp;unlink_item_ID='.$unlink_item->ID )
            .
'&amp;'.url_crumb( 'item' );
    }


   
/**
     * Provide a link to unlink a new version of this post if user has rights
     *
     * @param array Params:
     *  - 'unlink_item_ID': What item to unlink, NULL - to unlink this Item
     *  - 'before': to display before link
     *  - 'after':    to display after link
     *  - 'text': link text
     *  - 'title': link title
     *  - 'class': CSS class name
     * @return string
     */
   
function get_unlink_version_link( $params = array() )
    {
        if( ! (
$unlink_version_url = $this->get_unlink_version_url( $params ) ) )
        {    
// Unlink action is not allowed for current User and this Item
           
return false;
        }

       
// Default params:
       
$params = array_merge( array(
               
'unlink_item_ID' => NULL,
               
'before'         => ' ',
               
'after'          => ' ',
               
'text'           => '#icon#', // '#' - icon + text, '#icon#' - only icon, '#text#' - only text
               
'title'          => '#', // '#' - Unlink version...
               
'class'          => '',
            ),
$params );

        switch(
$params['text'] )
        {
            case
'#text#':
               
$params['text'] = T_('Unlink version').'...';
                break;

            case
'#':
               
$params['text'] = get_icon( 'unlink', 'imgtag', array( 'title' => T_('Unlink version').'...' ) ).' '.T_('Link version').'...';
                break;

            case
'#icon#':
               
$params['text'] = get_icon( 'unlink', 'imgtag', array( 'title' => T_('Unlink version').'...' ) );
                break;
        }

        if(
$params['title'] == '#' )
        {
           
$params['title'] = T_('Unlink version').'...';
        }

       
$ItemCache = & get_ItemCache();
        if( ! (
$unlink_item = & $ItemCache->get_by_ID( $params['unlink_item_ID'], false, false ) ) ||
           
$unlink_item->get( 'igrp_ID' ) != $this->get( 'igrp_ID' ) )
        {    
// Use current Item if the requested Item doesn't exist or it is from another group:
           
$unlink_item = $this;
        }

       
$r = $params['before'];

       
$item_Blog = & $this->get_Blog();
       
$r .= '<a href="'.$unlink_version_url.'" '
               
.'onclick="return confirm( \''.format_to_output( sprintf( TS_('Are you sure want to unlink the Item "%s" (%s)?'), $unlink_item->get( 'title' ), $unlink_item->get( 'locale' ) ), 'htmlattr' ).'\' )"'
               
.'title="'.format_to_output( $params['title'], 'htmlattr' ).'"'
               
.( empty( $params['class'] ) ? '' : ' class="'.$params['class'].'"' )
            .
'>'.format_to_output( $params['text'], 'htmlbody' ).'</a>';

       
$r .= $params['after'];

        return
$r;
    }


   
/**
     * Check to add/link a new version of this post if user has rights
     *
     * @return boolean
     */
   
function can_link_version( $allow_new_item = false )
    {
        if( !
$allow_new_item && ! $this->ID )
        {    
// Item must be saved in DB:
           
return false;
        }

        if( !
check_user_perm( 'item_post!CURSTATUS', 'edit', false, $this, false ) )
        {    
// User has no rights to edit this Item
           
return false;
        }

        if( !
is_admin_page() || ! check_user_perm( 'admin', 'restricted' ) )
        {    
// This feature is allowed only for back-office yet
           
return false;
        }

        return
true;
    }


   
/**
     * Provide link to edit a post if user has edit rights
     *
     * @param array Params:
     *  - 'before': to display before link
     *  - 'after':    to display after link
     *  - 'text': link text
     *  - 'title': link title
     *  - 'class': CSS class name
     *  - 'save_context': redirect to current URL?
     */
   
function get_edit_link( $params = array() )
    {
        global
$admin_url;

       
$actionurl = $this->get_edit_url($params);
        if( !
$actionurl )
        {
            return
false;
        }

       
// Make sure we are not missing any param:
       
$params = array_merge( array(
               
'before'       => ' ',
               
'after'        => ' ',
               
'text'         => '#',
               
'title'        => '#',
               
'class'        => '',
               
'save_context' => true,
            ),
$params );


        if(
$params['text'] == '#' ) $params['text'] = get_icon( 'edit' ).' '.T_('Edit...');
        if(
$params['text'] == '#icon#' ) $params['text'] = get_icon( 'edit' );
        if(
$params['title'] == '#' ) $params['title'] = T_('Edit this post...');

       
$r = $params['before'];
       
$r .= '<a href="'.$actionurl;
       
$r .= '" title="'.$params['title'].'"';
        if( !empty(
$params['class'] ) ) $r .= ' class="'.$params['class'].'"';
       
$r .=  '>'.$params['text'].'</a>';
       
$r .= $params['after'];

        return
$r;
    }


   
/**
     * Check if current User can edit this Item
     *
     * @return boolean
     */
   
function can_be_edited()
    {
       
// Item must be stored in DB:
       
return ! empty( $this->ID ) &&
           
// User must be logged in and activated and has a permission to edit this Item:
           
check_user_perm( 'item_post!CURSTATUS', 'edit', false, $this, false );
    }


   
/**
     * Get URL to edit a post if user has edit rights.
     *
     * @param array Params:
     *  - 'save_context': redirect to current URL?
     */
   
function get_edit_url( $params = array() )
    {
        global
$admin_url;

       
// default params
       
$params += array(
               
'save_context'             => true,
               
'glue'                     => '&amp;',
               
'force_in_skin_editing'    => false,
               
'force_backoffice_editing' => false,
               
'check_perm'               => true, // FALSE - if this link must be displayed even if current has no permission to view item history page
           
);

        if( empty(
$this->ID ) || ( $params['check_perm'] && ! $this->can_be_edited() ) )
        {    
// Don't allow to edit this Item if it is not created yet or if this Item cannot be edited by current User:
           
return false;
        }

       
$this->load_Blog();
       
$url = false;
        if(
$this->Blog->get_setting( 'in_skin_editing' ) &&
            ( !
$params['force_backoffice_editing'] || ! check_user_perm( 'admin', 'restricted' ) ) &&
            ( !
is_admin_page() || $params['force_in_skin_editing'] ) )
        {    
// We have a mode 'In-skin editing' for the current Blog
           
if( ! $params['check_perm'] || check_item_perm_edit( $this->ID, false ) )
            {    
// Current user can edit this post
               
$url = url_add_param( $this->Blog->get( 'url' ), 'disp=edit&p='.$this->ID );
            }
        }
        else if( !
$params['check_perm'] || check_user_perm( 'admin', 'restricted' ) )
        {    
// Edit a post from Back-office
           
$url = $admin_url.'?ctrl=items'.$params['glue'].'action=edit'.$params['glue'].'p='.$this->ID.$params['glue'].'blog='.$this->Blog->ID;
            if(
$params['save_context'] )
            {
               
$url .= $params['glue'].'redirect_to='.rawurlencode( regenerate_url( '', '', '', '&' ).'#'.$this->get_anchor_id() );
            }
        }
        return
$url;
    }


   
/**
     * Template tag
     * @see Item::get_edit_link()
     */
   
function edit_link( $params = array() )
    {
        echo
$this->get_edit_link( $params );
    }


   
/**
     * Provide link to propose change a post if user has edit rights
     *
     * @param array Params:
     *  - 'before': to display before link
     *  - 'after':    to display after link
     *  - 'text': link text
     *  - 'title': link title
     *  - 'class': CSS class name
     *  - 'save_context': redirect to current URL?
     */
   
function get_propose_change_link( $params = array() )
    {
       
$actionurl = $this->get_propose_change_url( $params );
        if( !
$actionurl )
        {    
// Don't display the propose change button if current user has no rights:
           
return false;
        }

       
// Make sure we are not missing any param:
       
$params = array_merge( array(
               
'before'       => ' ',
               
'after'        => ' ',
               
'text'         => '#',
               
'title'        => '#',
               
'class'        => '',
               
'save_context' => true,
            ),
$params );


        if(
$params['text'] == '#' )
        {    
// Default text:
           
$params['text'] = get_icon( 'edit_button' ).' '.T_('Propose change');
        }
        elseif(
$params['text'] == '#icon#' )
        {    
// Default text as single icon:
           
$params['text'] = get_icon( 'edit_button' );
        }
        if(
$params['title'] == '#' )
        {    
// Default title:
           
$params['title'] = T_('Propose change');
        }

        return
$params['before'].
           
'<a href="'.$actionurl.'"'.
                   
' title="'.format_to_output( $params['title'], 'htmlattr' ).'"'.
                    ( empty(
$params['class'] ) ? '' : ' class="'.format_to_output( $params['class'], 'htmlattr' ).'"' ).'>'.
               
$params['text'].
           
'</a>';
           
$params['after'];
    }


   
/**
     * Get URL to propose a change for a post if user has edit rights.
     *
     * @param array Params:
     *  - 'save_context': redirect to current URL?
     */
   
function get_propose_change_url( $params = array() )
    {
        global
$admin_url;

        if( !
$this->ID )
        {    
// Don't display this button in preview mode:
           
return false;
        }

        if( !
check_user_perm( 'blog_item_propose', 'edit', false, $this->get_blog_ID(), false ) )
        {    
// User has no right to propose a change for this Item:
           
return false;
        }

       
// default params:
       
$params += array( 'save_context' => true );

       
$this->load_Blog();
       
$url = false;
        if( !
is_admin_page() && $this->Blog->get_setting( 'in_skin_change_proposal' ) )
        {    
// We have a mode 'In-skin editing' for the current Blog
           
$url = url_add_param( $this->Blog->get( 'url' ), 'disp=proposechange&p='.$this->ID );
        }
        else if(
check_user_perm( 'admin', 'restricted' ) )
        {    
// Edit a post from Back-office:
           
$url = $admin_url.'?ctrl=items&amp;action=propose&amp;p='.$this->ID.'&amp;blog='.$this->Blog->ID;
            if(
$params['save_context'] )
            {
               
$url .= '&amp;redirect_to='.rawurlencode( regenerate_url( '', '', '', '&' ).'#'.$this->get_anchor_id() );
            }
        }

        return
$url;
    }


   
/**
     * Template tag
     * @see Item::get_edit_link()
     */
   
function propose_change_link( $params = array() )
    {
        echo
$this->get_propose_change_link( $params );
    }


   
/**
     * Get a link to view changes of Item for current User
     *
     * @return string A link to history
     */
   
function get_changes_link( $params = array() )
    {
       
$params = array_merge( array(
               
'before'    => '',
               
'after'     => '',
               
'link_text' => '#', // Use a mask $icon$ or some other text
               
'class'     => '',
            ),
$params );

        if( (
$changes_url = $this->get_changes_url() ) === false )
        {    
// No url available for current user, Don't display a link:
           
return;
        }

        if(
$params['link_text'] == '#' )
        {    
// Default link text:
           
$params['link_text'] = '$icon$ '.T_('View changes');
        }

       
// Replace all masks with values
       
$link_text = str_replace( '$icon$', $this->history_info_icon(), $params['link_text'] );

        return
$params['before']
            .
'<a href="'.$changes_url.'"'.( empty( $params['class'] ) ? '' : ' class="'.$params['class'].'"' ).'>'.$link_text.'</a>'
           
.$params['after'];
    }


   
/**
     * Get URL to view changes of Item for current User
     *
     * @param string Glue between url params
     * @return string|boolean URL to history OR False when user cannot see a history
     */
   
function get_changes_url( $glue = '&amp;' )
    {
        global
$admin_url;

        if( !
check_user_perm( 'item_post!CURSTATUS', 'edit', false, $this ) )
        {    
// Current user cannot see item changes:
           
return false;
        }

        if( !
$this->get_Blog()->get_setting( 'track_unread_content' ) )
        {    
// Tracking of unread content must be enabled to know last seen timestamp by current User:
           
return false;
        }

        if(
$this->get_read_status() != 'updated' )
        {    
// Don't allow URL when no new changes for current User:
           
return false;
        }

        return
$admin_url.'?ctrl=items'.$glue.'action=history_lastseen'.$glue.'p='.$this->ID;
    }


   
/**
     * Template tag
     * @see Item::get_changes_link()
     */
   
function changes_link( $params = array() )
    {
        echo
$this->get_changes_link( $params );
    }


   
/**
     * Get JavaScript code for onclick event of merge link
     *
     * @return boolean|string
     */
   
function get_merge_click_js()
    {
        if( !
$this->ID )
        {    
// Item must be stored in DB:
           
return false;
        }

        if( !
check_user_perm( 'item_post!CURSTATUS', 'edit', false, $this, false ) )
        {    
// User has no right to edit this Item:
           
return false;
        }

        return
'return evo_merge_load_window( '.$this->ID.' )';
    }


   
/**
     * Provide link to merge a post if user has edit rights
     *
     * @param array Params:
     *  - 'before': to display before link
     *  - 'after':    to display after link
     *  - 'text': link text
     *  - 'title': link title
     *  - 'class': CSS class name
     */
   
function get_merge_link( $params = array() )
    {
       
$merge_click_js = $this->get_merge_click_js( $params );
        if( !
$merge_click_js )
        {    
// Don't display the propose change button if current user has no rights:
           
return false;
        }

       
$params = array_merge( array(
               
'before' => ' ',
               
'after'  => ' ',
               
'text'   => '#',
               
'title'  => '#',
               
'class'  => '',
            ),
$params );

        if(
$params['text'] == '#' )
        {
           
$params['text'] = get_icon( 'merge' ).' '.T_('Merge with...');
        }
        elseif(
$params['text'] == '#icon#' )
        {
           
$params['text'] = get_icon( 'merge' );
        }
        if(
$params['title'] == '#' )
        {
           
$params['title'] = T_('Merge with...');
        }

       
$r = $params['before'];
       
$r .= '<a href="#" onclick="'.$merge_click_js.'"'
                   
.' title="'.$params['title'].'"'
                   
.( empty( $params['class'] ) ? '' : ' class="'.$params['class'].'"' ).'>'
               
.$params['text']
            .
'</a>';
       
$r .= $params['after'];

        return
$r;
    }


   
/**
     * Template tag
     * @see Item::get_merge_link()
     */
   
function merge_link( $params = array() )
    {
       
$merge_link = $this->get_merge_link( $params );

        if( ! empty(
$merge_link ) )
        {
           
echo_item_merge_js();
            echo
$merge_link;
        }
    }


   
/**
     * Get next status to publish/restrict to this item
     * TODO: asimo>Refactor this with Comment->get_next_status()
     *
     * @param boolean true to get next publish status, and false to get next restrict status
     * @return mixed false if user has no permission | array( status, status_text, icon_color ) otherwise
     */
   
function get_next_status( $publish )
    {
        if( !
is_logged_in( false ) )
        {
            return
false;
        }

       
$status_order = get_visibility_statuses( 'ordered-array' );
       
$status_index = get_visibility_statuses( 'ordered-index' );

       
$curr_index = $status_index[$this->status];
        if( ( !
$publish ) && ( $curr_index == 0 ) && ( $this->status != 'deprecated' ) )
        {
           
$curr_index = $curr_index + 1;
        }
       
$has_perm = false;
        while( !
$has_perm && ( $publish ? ( $curr_index < 4 ) : ( $curr_index > 0 ) ) )
        {
           
$curr_index = $publish ? ( $curr_index + 1 ) : ( $curr_index - 1 );
           
$has_perm = check_user_perm( 'item_post!'.$status_order[$curr_index][0], 'moderate', false, $this );
        }
        if(
$has_perm )
        {
           
$label_index = $publish ? 1 : 2;
            return array(
$status_order[$curr_index][0], $status_order[$curr_index][$label_index], $status_order[$curr_index][3] );
        }
        return
false;
    }


   
/**
     * Provide link to publish a post if user has edit rights
     *
     * Note: publishing date will be updated
     *
     * @param string to display before link
     * @param string to display after link
     * @param string link text
     * @param string link title
     * @param string class name
     * @param string glue between url params
     */
   
function get_publish_link( $before = ' ', $after = ' ', $text = '#', $title = '#', $class = '', $glue = '&amp;', $save_context = true )
    {
        global
$admin_url;

        if(
$this->status != 'draft' )
        {
            return
false;
        }

        if( !
check_user_perm( 'item_post!published', 'edit', false, $this, false ) ||
            !
check_user_perm( 'blog_edit_ts', 'edit', false, $this->get_blog_ID(), false ) )
        {
// User has no right to publish this post now:
           
return false;
        }

        if(
$text == '#' ) $text = get_icon( 'post', 'imgtag' ).' '.T_('Publish NOW!');
        if(
$title == '#' ) $title = T_('Publish now using current date and time.');

       
$r = $before;
       
$r .= '<a href="'.$admin_url.'?ctrl=items'.$glue.'action=publish_now'.$glue.'post_ID='.$this->ID.$glue.url_crumb('item');
        if(
$save_context )
        {
           
$r .= $glue.'redirect_to='.rawurlencode( regenerate_url( '', '', '', '&' ) );
        }
       
$r .= '" title="'.$title.'"';
        if( !empty(
$class ) ) $r .= ' class="'.$class.'"';
       
$r .= '>'.$text.'</a>';
       
$r .= $after;

        return
$r;
    }


   
/**
     * Provide link to publish a post to the highest available public status for the current User
     *
     * @param $params
     * @return boolean true if link was displayed false otherwise
     */
   
function highest_publish_link( $params = array() )
    {
        global
$admin_url;

        if( !
is_logged_in( false ) )
        {
            return
false;
        }

       
$params = array_merge( array(
               
'before'       => '',
               
'after'        => '',
               
'text'         => '#',
               
'before_text'  => '',
               
'after_text'   => '',
               
'title'        => '',
               
'class'        => '',
               
'glue'         => '&amp;',
               
'save_context' => true,
               
'redirect_to'  => '',
            ),
$params );

       
$curr_status_permvalue = get_status_permvalue( $this->status );
       
// get the current User highest publish status for this item Blog
       
list( $highest_status, $publish_text ) = get_highest_publish_status( 'post', $this->get_blog_ID(), true, '', $this );
       
// Get binary value of the highest available status
       
$highest_status_permvalue = get_status_permvalue( $highest_status );
        if(
$curr_status_permvalue >= $highest_status_permvalue || ( $highest_status_permvalue <= get_status_permvalue( 'private' ) ) )
        {
// Current User has no permission to change this comment status to a more public status
           
return false;
        }

        if( ! (
check_user_perm( 'item_post!'.$highest_status, 'edit', false, $this ) ) )
        {
// User has no right to edit this post
           
return false;
        }

       
$glue = $params[ 'glue' ];
       
$text = ( $params[ 'text' ] == '#' ) ? $publish_text : $params[ 'text' ];

       
$r = $params[ 'before' ];
       
$r .= '<a href="'.$admin_url.'?ctrl=items'.$glue.'action=publish'.$glue.'post_status='.$highest_status.$glue.'post_ID='.$this->ID.$glue.url_crumb('item');
        if(
$params[ 'redirect_to' ] )
        {
           
$r .= $glue.'redirect_to='.rawurlencode( $params[ 'redirect_to' ] );
        }
        elseif(
$params[ 'save_context' ] )
        {
           
$r .= $glue.'redirect_to='.rawurlencode( regenerate_url( '', '', '', '&' ) );
        }
       
$r .= '" title="'.$params[ 'title' ].'"';
        if( !empty(
$params[ 'class' ] ) ) $r .= ' class="'.$params[ 'class' ].'"';
       
$r .= '>'.$params[ 'before_text' ].$text.$params[ 'after_text' ].'</a>';
       
$r .= $params[ 'after' ];

        echo
$r;
        return
true;
    }


   
/**
     * Provide link to publish a post if user has permission
     *
     * @param $params
     * @return boolean true if link was displayed false otherwise
     */
   
function publish_link( $before = ' ', $after = ' ', $text = '#', $title = '#', $class = '', $glue = '&amp;', $save_context = true )
    {
       
$publish_link = $this->get_publish_link( $before, $after, $text, $title, $class, $glue, $save_context );

        if(
$publish_link === false )
        {    
// The publish link is unavailable for current user and for this item
           
return false;
        }

       
// Display the publish link
       
echo $publish_link;

        return
true;
    }


   
/**
     * Display next Publish/Restrict to link
     *
     * @param array link params
     * @param boolean true to display next publish status, and false to display next restrict status link
     * @return boolean true if link was displayed | false otherwise
     */
   
function next_status_link( $params, $publish )
    {
        global
$admin_url;

       
$params = array_merge( array(
               
'before'      => '',
               
'after'       => '',
               
'before_text' => '',
               
'after_text'  => '',
               
'text'        => '#',
               
'title'       => '',
               
'class'       => '',
               
'glue'        => '&amp;',
               
'redirect_to' => '',
               
'post_navigation' => 'same_blog',
               
'nav_target'  => NULL,
            ),
$params );

        if(
$publish )
        {
           
$next_status_in_row = $this->get_next_status( true );
           
$action = 'publish';
           
$button_default_icon = isset( $next_status_in_row[2] ) ? 'move_up_'.$next_status_in_row[2] : 'move_up_';
        }
        else
        {
           
$next_status_in_row =  $this->get_next_status( false );
           
$action = 'restrict';
           
$button_default_icon = isset( $next_status_in_row[2] ) ? 'move_down_'.$next_status_in_row[2] : 'move_down_';
        }

        if(
$next_status_in_row === false )
        {
// Next status is not allowed for current user
           
return false;
        }

       
$next_status = $next_status_in_row[0];
       
$next_status_label = $next_status_in_row[1];

        if( isset(
$params['text_'.$next_status] ) )
        {
// Set text from params for next status
           
$text = $params['text_'.$next_status];
        }
        elseif(
$params['text' ] != '#' )
        {
// Set text from params for any atatus
           
$text = $params['text'];
        }
        else
        {
// Default text
           
$text = get_icon( $button_default_icon, 'imgtag', array( 'title' => '' ) ).' '.$next_status_label;
        }

        if( empty(
$params['title'] ) )
        {
           
$status_title = get_visibility_statuses( 'moderation-titles' );
           
$params['title'] = $status_title[$next_status];
        }
       
$glue = $params['glue'];

       
$r = $params['before'];
       
$r .= '<a href="'.$admin_url.'?ctrl=items'.$glue.'action='.$action.$glue.'post_status='.$next_status.$glue.'post_ID='.$this->ID.$glue.url_crumb('item');

       
// set redirect_to
       
$redirect_to = $params['redirect_to'];
        if( empty(
$redirect_to ) && ( !is_admin_page() ) )
        {
// we are in front office
           
if( $next_status == 'deprecated' )
            {
                if(
$params['post_navigation'] == 'same_category' )
                {
                   
$redirect_to = get_caturl( $params['nav_target'] );
                }
                else
                {
                   
$this->get_Blog();
                   
$redirect_to = $this->Blog->gen_blogurl();
                }
            }
            else
            {
               
$redirect_to = $this->add_navigation_param( $this->get_permanent_url(), $params['post_navigation'], $params['nav_target'] );
            }
        }
        if( !empty(
$redirect_to ) )
        {
           
$r .= $glue.'redirect_to='.rawurlencode( $redirect_to );
        }

       
$r .= '" title="'.$params['title'].'"';
        if( empty(
$params['class_'.$next_status] ) )
        {
// Set class for all statuses
           
$class = empty( $params['class'] ) ? '' : $params['class'];
        }
        else
        {
// Set special class for next status
           
$class = $params['class_'.$next_status];
        }
        if( !empty(
$class ) ) $r .= ' class="'.$class.'"';
       
$r .= '>'.$params['before_text'].$text.$params['after_text'].'</a>';
       
$r .= $params['after'];

        echo
$r;
        return
true;
    }


   
/**
     * Provide link to deprecate a post if user has edit rights
     *
     * @param string to display before link
     * @param string to display after link
     * @param string link text
     * @param string link title
     * @param string class name
     * @param string glue between url params
     */
   
function get_deprecate_link( $before = ' ', $after = ' ', $text = '#', $title = '#', $class = '', $glue = '&amp;', $redirect_to = '' )
    {
        global
$admin_url;

        if(
$this->status == 'deprecated' || // Already deprecated!
           
! check_user_perm( 'item_post!deprecated', 'edit', false, $this, false ) )
        {
// User has no right to deprecated this post:
           
return false;
        }

        if(
$text == '#' ) $text = get_icon( 'deprecate', 'imgtag' ).' '.T_('Deprecate').'!';
        if(
$title == '#' ) $title = T_('Deprecate this post!');

        if( !empty(
$redirect_to ) )
        {
           
$redirect_to = $glue.'redirect_to='.rawurlencode( $redirect_to );
        }

       
$r = $before;
       
$r .= '<a href="'.$admin_url.'?ctrl=items'.$glue.'action=deprecate'.$glue.'post_ID='.$this->ID.$glue.url_crumb('item').$redirect_to;
       
$r .= '" title="'.$title.'"';
        if( !empty(
$class ) ) $r .= ' class="'.$class.'"';
       
$r .= '>'.$text.'</a>';
       
$r .= $after;

        return
$r;
    }


   
/**
     * Display link to deprecate a post if user has edit rights
     *
     * @param string to display before link
     * @param string to display after link
     * @param string link text
     * @param string link title
     * @param string class name
     * @param string glue between url params
     */
   
function deprecate_link( $before = ' ', $after = ' ', $text = '#', $title = '#', $class = '', $glue = '&amp;', $redirect_to = '' )
    {
       
$deprecate_link = $this->get_deprecate_link( $before, $after, $text, $title, $class, $glue, $redirect_to );

        if(
$deprecate_link === false )
        {    
// The deprecate link is unavailable for current user and for this item
           
return false;
        }

       
// Display the deprecate link
       
echo $deprecate_link;

        return
true;
    }


   
/**
     * Template function: display priority of item
     *
     * @param string
     * @param string
     */
   
function priority( $before = '', $after = '' )
    {
        if( isset(
$this->priority) )
        {
            echo
$before;
            echo
$this->priority;
            echo
$after;
        }
    }


   
/**
     * Get checkable list of renderers
     *
     * @param array|NULL If given, assume these renderers to be checked.
     * @return string Renderer checkboxes
     */
   
function get_renderer_checkboxes( $item_renderers = NULL )
    {
        global
$Plugins;

        if(
is_null( $item_renderers ) )
        {
           
$item_renderers = $this->get_renderers();
        }

        return
$Plugins->get_renderer_checkboxes( $item_renderers, array( 'Item' => & $this ) );
    }


   
/**
     * Template function: display checkable list of renderers
     *
     * @param array|NULL If given, assume these renderers to be checked.
     */
   
function renderer_checkboxes( $item_renderers = NULL )
    {
        echo
$this->get_renderer_checkboxes( $item_renderers );
    }


   
/**
     * Get status of item
     *
     * Statuses:
     * - published
     * - deprecated
     * - protected
     * - private
     * - draft
     *
     * @param array Params
     */
   
function get_status( $params = array() )
    {
       
// Make sure we are not missing any param:
       
$params = array_merge( array(
               
'before' => '',
               
'after'  => '',
               
'format' => 'htmlbody', // DO NOT USE 'styled' ->DEPRECATED! INSTEAD: Valid values: see {@link format_to_output()}
               
'class'  => '',            // DEPRECATED
           
), $params );

       
$r = $params['before'];

        switch(
$params['format'] )
        {
            case
'raw':
               
$r .= $this->get_status_raw();
                break;

            case
'styled':
               
// DEPRECATED: instead use something like: $Item->format_status( array(    'template' => '<div class="evo_status__banner evo_status__$status$">$status_title$</div>' ) );
               
$r .= get_styled_status( $this->status, $this->get('t_status'), $params['class'] );
                break;

            default:
// other formats
               
$r .= format_to_output( $this->get('t_status'), $params['format'] );
                break;
        }

       
$r .= $params['after'];

        return
$r;
    }


   
/**
     * Template function: display status of item
     *
     * Statuses:
     * - published
     * - deprecated
     * - protected
     * - private
     * - draft
     *
     * @param array Params
     */
   
function status( $params = array() )
    {
       
// Make sure we are not missing any param:
       
$params = array_merge( array(
               
'before' => '',
               
'after'  => '',
               
'format' => 'htmlbody', // Output format, see {@link format_to_output()}
               
'class'  => '',
            ),
$params );

        echo
$this->get_status( $params );
    }


   
/**
     * Get status of item in a formatted way, following a provided template
     *
     * There are 3 possible variables:
     * - $status$ = the raw status
     * - $status_title$ = the human readable text version of the status (translated to current language)
     * - $tooltip_title$ = the human readable text version of the status for the tooltip
     *
     * @param array Params
     * @return string
     */
   
function get_format_status( $params = array() )
    {
       
$params = array_merge( array(
               
'template' => '<div class="evo_status evo_status_$status$" data-toggle="tooltip" data-placement="top" title="$tooltip_title$">$status_title$</div>',
               
'format'   => 'htmlbody', // Output format, see {@link format_to_output()}
           
), $params );

       
$r = str_replace( '$status$', $this->get( 'status' ), $params['template'] );
       
$r = str_replace( '$status_title$', $this->get('t_status'), $r );
       
$r = str_replace( '$tooltip_title$', get_status_tooltip_title( $this->get( 'status' ) ), $r );

        return
format_to_output( $r, $params['format'] );
    }


   
/**
     * Display status of item in a formatted way, following a provided template
     *
     * There are 2 possible variables:
     * - $status$ = the raw status
     * - $status_title$ = the human readable text version of the status (translated to current language)
     *
     * @param array Params
     */
   
function format_status( $params = array() )
    {
        echo
$this->get_format_status( $params );
    }


   
/**
     * Output classes for the Item <div>
     */
   
function div_classes( $params = array(), $output = true )
    {
        global
$disp;

       
// Make sure we are not missing any param:
       
$params = array_merge( array(
               
'item_class'        => 'bPost',
               
'item_type_class'   => 'bPost_ptyp',
               
'item_status_class' => 'bPost',
            ),
$params );

       
$classes = array( $params['item_class'],
               
$params['item_type_class'].$this->ityp_ID,
               
$params['item_status_class'].$this->status,
            );

       
$r = implode( ' ', $classes );

        if( !
$output ) return $r;

        echo
$r;
    }


   
/**
     * Get raw status
     *
     * @return string Status
     */
   
function get_status_raw()
    {
        return
$this->status;
    }


   
/**
     * Output raw status.
     */
   
function status_raw()
    {
        echo
$this->get_status_raw();
    }


   
/**
     * Template function: display extra status of item
     *
     * @param string
     * @param string
     * @param string Output format, see {@link format_to_output()}
     */
   
function extra_status( $before = '', $after = '', $format = 'htmlbody' )
    {
        if(
$format == 'raw' )
        {
           
$this->disp( $this->get('t_extra_status'), 'raw' );
        }
        elseif(
$extra_status = $this->get('t_extra_status') )
        {
            echo
$before.format_to_output( $extra_status, $format ).$after;
        }
    }


     
/**
     * Display tags for Item
     *
     * @param array of params
     * @param string Output format, see {@link format_to_output()}
     */
   
function tags( $params = array() )
    {
        global
$evo_charset;

       
$params = array_merge( array(
               
'before'     => '<div>'.T_('Tags').': ',
               
'after'      => '</div>',
               
'separator'  => ', ',
               
'links'      => true,
               
'before_tag' => '',
               
'after_tag'  => '',
            ),
$params );

       
$tags = $this->get_tags();

        if( !empty(
$tags ) )
        {
            echo
$params['before'];

            if(
$links = $params['links'] )
            {
               
$this->get_Blog();
            }

           
$i = 0;
            foreach(
$tags as $tag_ID => $tag_name )
            {
                if(
$i++ > 0 )
                {
                    echo
$params['separator'];
                }

                echo
str_replace( '$tag_ID$', $tag_ID, $params['before_tag'] );
                if(
$links )
                {    
// We want links
                   
echo $this->Blog->get_tag_link( $tag_name );
                }
                else
                {
                    echo
htmlspecialchars( $tag_name, NULL, $evo_charset );
                }
                echo
$params['after_tag'];
            }

            echo
$params['after'];
        }
    }


   
/**
     * Template function: Displays trackback autodiscovery information
     *
     * TODO: build into headers
     */
   
function trackback_rdf()
    {
       
$this->get_Blog();
        if( !
$this->can_receive_pings() )
        {
// Trackbacks not allowed on this blog:
           
return;
        }

        echo
'<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" '."\n";
        echo
'  xmlns:dc="http://purl.org/dc/elements/1.1/"'."\n";
        echo
'  xmlns:trackback="http://madskills.com/public/xml/rss/module/trackback/">'."\n";
        echo
'<rdf:Description'."\n";
        echo
'  rdf:about="';
       
$this->permanent_url( 'single' );
        echo
'"'."\n";
        echo
'  dc:identifier="';
       
$this->permanent_url( 'single' );
        echo
'"'."\n";
       
$this->title( array(
           
'before'    => ' dc:title="',
           
'after'     => '"'."\n",
           
'link_type' => 'none',
           
'format'    => 'xmlattr',
            ) );
        echo
'  trackback:ping="';
       
$this->trackback_url();
        echo
'" />'."\n";
        echo
'</rdf:RDF>';
    }


   
/**
     * Template function: displays url to use to trackback this item
     */
   
function trackback_url()
    {
        echo
$this->get_trackback_url();
    }


   
/**
     * Template function: get url to use to trackback this item
     * @return string
     */
   
function get_trackback_url()
    {
       
// fp> TODO: get a clean (per blog) setting for this
        //    return get_htsrv_url().'trackback.php/'.$this->ID;

       
return get_htsrv_url().'trackback.php?tb_id='.$this->ID;
    }


   
/**
     * Get HTML code to display video/audio player for playback of a given URL
     *
     * @param string The URL of video/audio file
     * @return string The HTML code
     */
   
function get_player( $url )
    {
        global
$Plugins;

       
$params = array(
               
'url'  => $url,
               
'data' => '',
            );

       
$temp_params = $params;
        foreach(
$params as $param_key => $param_value )
        {
// Pass all params by reference, in order to give possibility to modify them by plugin
            // So plugins can add some data before/after image tags (E.g. used by infodots plugin)
           
$params[ $param_key ] = & $params[ $param_key ];
        }

       
$params = $Plugins->trigger_event_first_true( 'RenderURL', $params );
        if(
count( $params ) != 0 )
        {    
// Display a rendered url, for example as video/audio player:
           
return $params['data'];
        }

       
// Display URL as simple link:
       
return '<a href="'.$url.'">'.$url.'</a>';
    }


   
/**
     * Template function: Display link to item related url.
     *
     * By default the link is displayed as a link.
     * Optionally some smart stuff may happen.
     */
   
function url_link( $params = array() )
    {

        if( empty(
$this->url ) )
        {
            return;
        }

       
// Make sure we are not missing any param:
       
$params = array_merge( array(
               
'before'        => ' ',
               
'after'         => ' ',
               
'text_template' => '$url$',        // If evaluates to empty, nothing will be displayed (except player if podcast)
               
'url_template'  => '$url$',
               
'target'        => '',
               
'format'        => 'htmlbody',
               
'podcast'       => '#',                        // handle as podcast. # means depending on post type
               
'before_podplayer' => '<div class="podplayer">',
               
'after_podplayer'  => '</div>',
               
'link_class'     => ''
           
), $params );

        if(
$params['podcast'] == '#' )
        {    
// Check if this post is a podcast:
           
$params['podcast'] = $this->get_type_setting( 'podcast' );
        }

        if(
$params['podcast'] && $params['format'] == 'htmlbody' )
        {    
// We want podcast display:

           
echo $params['before_podplayer'];

            echo
$this->get_player( $this->url );

            echo
$params['after_podplayer'];

        }
        else
        {
// Not displaying podcast player:

           
$text = str_replace( '$url$', $this->url, $params['text_template'] );
            if( empty(
$text) )
            {    
// Nothing to display
               
return;
            }

           
$r = $params['before'];

           
$r .= '<a href="'.str_replace( '$url$', $this->url, $params['url_template'] ).'"';

            if( !empty(
$params['link_class'] ) )
            {
               
$r .= ' class="'.$params['link_class'].'"';
            }

            if( !empty(
$params['target'] ) )
            {
               
$r .= ' target="'.$params['target'].'"';
            }

           
$r .= '>'.$text.'</a>';

           
$r .= $params['after'];

            echo
format_to_output( $r, $params['format'] );
        }
    }


   
/**
     * Template function: Display link to parent of this item.
     *
     * @param array
     */
   
function parent_link( $params = array() )
    {
        if( empty(
$this->parent_ID ) )
        {    
// No parent
           
return;
        }

        if(
$this->get_type_setting( 'use_parent' ) == 'never' )
        {    
// This item cannot has a parent item, because of item type settings
           
return;
        }

       
// Make sure we are not missing any param:
       
$params = array_merge( array(
               
'before'         => '',
               
'after'          => '',
               
'not_found_text' => '',
               
'format'         => 'htmlbody',
            ),
$params );

       
// Get parent Item:
       
$parent_Item = $this->get_parent_Item();

       
$r = $params['before'];

        if( ! empty(
$parent_Item ) )
        {    
// Display a parent post title as link to permanent url
           
$r .= $parent_Item->get_title();
        }
        else
        {    
// No parent post found, Display a text to inform about this:
           
$r .= $params['not_found_text'];
        }

       
$r .= $params['after'];

        echo
format_to_output( $r, $params['format'] );
    }


   
/**
     * Template function: Display the number of words in the post
     */
   
function wordcount()
    {
        echo (int)
$this->wordcount; // may have been saved as NULL until 1.9
   
}


   
/**
     * Template function: Display the number of times the Item has been viewed
     *
     * Note: viewcount is incremented whenever the Item's content is displayed with "MORE"
     * (i-e full content), see {@link Item::content()}.
     *
     * Viewcount is NOT incremented on page reloads and other special cases, see {@link Hit::is_new_view()}
     *
     * %d gets replaced in all params by the number of views.
     *
     * @deprecated Deprecated
     * @param string Link text to display when there are 0 views
     * @param string Link text to display when there is 1 views
     * @param string Link text to display when there are >1 views
     * @return string The phrase about the number of views.
     */
   
function get_views( $zero = '#', $one = '#', $more = '#' )
    {
       
// Deprecated feature, Display nothing:
       
return '';
    }


   
/**
     * Template function: Display a phrase about the number of Item views.
     *
     * @deprecated Deprecated
     * @param string Link text to display when there are 0 views
     * @param string Link text to display when there is 1 views
     * @param string Link text to display when there are >1 views (include %d for # of views)
     * @return integer Number of views.
     */
   
function views( $zero = '#', $one = '#', $more = '#' )
    {
       
// Deprecated feature, Display nothing:
       
return 0;
    }


   
/**
     * Set param value
     *
     * By default, all values will be considered strings
     *
     * @todo extra_cat_IDs recording
     *
     * @param string parameter name
     * @param mixed parameter value
     * @param boolean true to set to NULL if empty value
     * @return boolean true, if a value has been set; false if it has not changed
     */
   
function set( $parname, $parvalue, $make_null = false )
    {
        switch(
$parname )
        {
            case
'pst_ID':
            case
'priority':
                return
$this->set_param( $parname, 'number', $parvalue, true );

            case
'content':
               
$this->content_is_updated = $this->set_param( 'content', 'string', $parvalue, $make_null );
               
// Update wordcount as well:
               
$wordcount_is_updated = $this->set_param( 'wordcount', 'number', bpost_count_words( $this->content ), false );
                return (
$this->content_is_updated || $wordcount_is_updated );
               
// fp>: I think we should remove return above and add the following code:
                // BUT we cannot do that because generating the except requires to execute renderers, which requires to have a main cat ID/coll ID set, which we may not have at this point
                /*                if( $this->excerpt_autogenerated )
                                {    // As far as we know, we are still auto-generating excerpts for this Item at this moment,
                                    //        so let's make sure the excerpt stays in sync:
                                    $r3 = $this->set_param( 'excerpt', 'string', $this->get_autogenerated_excerpt() )
                                }
                                return ( $this->content_is_updated || $wordcount_is_updated || $r3 );
                */

           
case 'wordcount':
            case
'featured':
                return
$this->set_param( $parname, 'number', $parvalue, false );

            case
'datedeadline':
                return
$this->set_param( 'datedeadline', 'date', $parvalue, true );

            case
'order':
               
// Field 'post_order' is deprecated,
                // but we can set it per each category:
               
if( is_array( $this->extra_cat_IDs ) )
                {    
// Update order per each item category:
                   
$this->orders = array();
                    foreach(
$this->extra_cat_IDs as $extra_cat_ID )
                    {
                       
$this->orders[ $extra_cat_ID ] = $parvalue;
                    }
                }
                return
false;

            case
'renderers': // deprecated
               
return $this->set_renderers( $parvalue );

            case
'excerpt':
                if( empty(
$parvalue ) )
                {    
// We are trying to make the excerpt empty.
                    // This means we should to go back to autogenerated excerpt:
                    // fp>TODO (LATER): maybe we should do this only if $this->get_type_setting( 'use_excerpt' ) == 'required' ???
                   
$this->set( 'excerpt_autogenerated', 1 );
                   
$parvalue = $this->get_autogenerated_excerpt();
                }
                return
parent::set( 'excerpt', $parvalue, $make_null );

            case
'notifications_flags':
               
$notifications_flags = $this->get( 'notifications_flags' );
                if( !
is_array( $parvalue ) )
                {    
// Convert string to array:
                   
$parvalue = array( $parvalue );
                }
               
$notifications_flags = array_merge( $notifications_flags, $parvalue );
               
$notifications_flags = array_unique( $notifications_flags );
                return
$this->set_param( 'notifications_flags', 'string', implode( ',', $notifications_flags ), $make_null );

            case
'status':
               
// We need to set a reminder here to later check if the new status is allowed at dbinsert or dbupdate time ( $this->restrict_status( true ) )
                // We cannot check immediately because we may be setting the status before having set a main cat_ID -> a collection ID to check the status possibilities
                // Save previous status as a reminder (it can be useful to compare later. The Comment class uses this).
               
$this->previous_status = $this->get( 'status' );
                return
parent::set( 'status', $parvalue, $make_null );

            case
'revision':
                if( ! isset(
$this->revision ) || $this->revision != $parvalue )
                {    
// When revision is changed:
                   
if( isset( $this->custom_fields ) )
                    {    
// Reset custom fields:
                       
unset( $this->custom_fields );
                    }
                   
// Clear Links/Attachments to load them with data from other revision:
                   
$LinkCache = & get_LinkCache();
                   
$LinkCache->clear();
                }
                if(
$parvalue == 'c' )
                {    
// Don't set revision if required to current version:
                   
if( $this->is_revision() )
                    {    
// Unset previous active revision:
                       
unset( $this->revision );
                    }
                    return
false;
                }
               
// Don't use parent::set() because post has no column "revision" in DB:
               
$this->revision = $parvalue;
                return
true;

            case
'urltitle':
                if( ! isset(
$this->previous_urltitle ) )
                {    
// Save previous urltitle, may be used to rename folder of attachments:
                   
$this->previous_urltitle = $this->get( 'urltitle' );
                }
                return
parent::set( $parname, $parvalue, $make_null );

            default:
                return
parent::set( $parname, $parvalue, $make_null );
        }
    }


   
/**
     * Set the renderers of the Item.
     *
     * @param array List of renderer codes.
     * @return boolean true, if it has been set; false if it has not changed
     */
   
function set_renderers( $renderers )
    {
        return
$this->set_param( 'renderers', 'string', implode( '.', $renderers ) );
    }


   
/**
     * Set the Author of the Item.
     *
     * @param User (Do NOT set to NULL or you may kill the current_User)
     * @return boolean true, if it has been set; false if it has not changed
     */
   
function set_creator_User( & $creator_User )
    {
       
$this->creator_User = & $creator_User;
       
$this->Author = & $this->creator_User; // deprecated  fp> TODO: Test and see if this line can be put once and for all in the constructor
       
return $this->set( $this->creator_field, $creator_User->ID );
    }


   
/**
     * Set the Item location from the current user. Use to create a new post.
     *
     * @param string Location (country | region | subregion | city)
     */
   
function set_creator_location( $location )
    {
        global
$current_User;

        if( !
is_logged_in() )
        {    
// No logged in user
           
return;
        }

       
$locations = array(
               
'country'   => 'ctry_ID',
               
'region'    => 'rgn_ID',
               
'subregion' => 'subrg_ID',
               
'city'      => 'city_ID',
            );

       
$field_ID = $locations[$location];

       
$this->load_Blog();
        if(
$this->{$location.'_visible'}() )
        {    
// Location is visible
           
if( empty( $this->$field_ID ) )
            {    
// Set default location
               
$this->set( $field_ID, $current_User->$field_ID );
            }
        }
    }


   
/**
     * Create a new Item/Post and insert it into the DB
     *
     * This function has to handle all needed DB dependencies!
     *
     * @deprecated Use set() + dbinsert() instead
     */
   
function insert(
       
$author_user_ID,              // Author
       
$post_title,
       
$post_content,
       
$post_timestamp,              // 'Y-m-d H:i:s'
       
$main_cat_ID = 1,             // Main cat ID
       
$extra_cat_IDs = array(),     // Table of extra cats
       
$post_status = 'published',   // Use first char '!' before status name to force this status without restriction by max allowed status of the collection
       
$post_locale = '#',
       
$post_urltitle = '',
       
$post_url = '',
       
$post_comment_status = 'open',
       
$post_renderers = array('default'),
       
$item_type_name_or_ID_or_template = '#', // Use 'Page', 'Post' and etc. OR '#' to use default post type OR integer to use post type by ID OR $template_name$
       
$item_st_ID = NULL,
       
$postcat_order = NULL,
       
$display_restrict_status_messages = true )
    {
        global
$DB, $query, $UserCache;
        global
$default_locale;

        if(
$item_type_name_or_ID_or_template == '#' )
        {    
// Try to set default post type ID from blog setting:
           
$ChapterCache = & get_ChapterCache();
            if(
$Chapter = & $ChapterCache->get_by_ID( $main_cat_ID, false, false ) &&
               
$Collection = $Blog = & $Chapter->get_Blog() )
            {    
// Use default post type what used for the blog:
               
$item_typ_ID = $Blog->get_setting( 'default_post_type' );
            }
        }
        else
        {    
// Try to get item type by requested name:
           
$ItemTypeCache = & get_ItemTypeCache();
            if(
is_int( $item_type_name_or_ID_or_template ) &&
                (
$ItemType = & $ItemTypeCache->get_by_ID( $item_type_name_or_ID_or_template, false, false ) ) )
            {    
// Item type exists in DB by requested ID, Use it:
               
$item_typ_ID = $ItemType->ID;
            }
            elseif(
preg_match( '/^\$(.+)\$$/', $item_type_name_or_ID_or_template, $ityp_match ) &&
                    (
$ItemType = & $ItemTypeCache->get_by_template( $ityp_match[1], false, false ) ) )
            {    
// Item type exists in DB by requested template, Use it:
               
$item_typ_ID = $ItemType->ID;
            }
            elseif(
$ItemType = & $ItemTypeCache->get_by_name( $item_type_name_or_ID_or_template, false, false ) )
            {    
// Item type exists in DB by requested name, Use it:
               
$item_typ_ID = $ItemType->ID;
            }
        }

        if(
$post_comment_status == 'closed' || $post_comment_status == 'disabled' )
        {    
// Check if item type allows these options:
           
$ItemTypeCache = & get_ItemTypeCache();
           
$ItemType = & $ItemTypeCache->get_by_ID( $item_typ_ID );
            if(
$post_comment_status == 'closed' && ! $ItemType->get( 'allow_closing_comments' ) )
            {
               
debug_die( 'Item type "'.$ItemType->get_name().'" doesn\'t support closing comments, please set another comment status for item "'.$post_title.'"' );
            }
            elseif(
$post_comment_status == 'disabled' && ! $ItemType->get( 'allow_disabling_comments' ) )
            {
               
debug_die( 'Item type "'.$ItemType->get_name().'" doesn\'t support disabling comments, please set another comment status for item "'.$post_title.'"' );
            }
        }

        if( empty(
$item_typ_ID ) )
        {    
// Use first item type by default for wrong request:
           
$item_typ_ID = 1;
        }

       
// Set Item Type here in order to get item type settings below:
       
$this->set( 'ityp_ID', $item_typ_ID );

        if( !
$this->get_type_setting( 'allow_html' ) )
        {    
// Strip HTML tags from content if HTML is not allowed for Item Type of this Item:
           
$post_content = utf8_strip_tags( $post_content );
        }

        if(
$post_locale == '#' ) $post_locale = $default_locale;

       
// echo 'INSERTING NEW POST ';

       
if( isset( $UserCache ) )    // DIRTY HACK
       
{ // If not in install procedure...
           
$this->set_creator_User( $UserCache->get_by_ID( $author_user_ID ) );
        }
        else
        {
           
$this->set( $this->creator_field, $author_user_ID );
        }
       
$this->set( $this->lasteditor_field, $this->{$this->creator_field} );
       
$this->set( 'title', $post_title );
       
$this->set( 'urltitle', $post_urltitle );
       
$this->set( 'content', $post_content );
       
//$this->set( 'datestart', $post_timestamp );
       
$this->set( 'datestart', date( 'Y-m-d H:i:s' ) ); // Use current time temporarily, we'll update this later

       
$this->set( 'main_cat_ID', $main_cat_ID );
       
$this->set( 'extra_cat_IDs', $extra_cat_IDs );
        if(
substr( $post_status, 0, 1 ) == '!' )
        {    
// Force the requested status to ignore restriction by collection settings:
           
$post_status = substr( $post_status, 1 );
           
$force_status = true;
        }
       
$this->set( 'status', $post_status );
        if( ! empty(
$force_status ) )
        {    
// Unset flag to don't restrict the status:
           
unset( $this->previous_status );
        }
       
$this->set( 'locale', $post_locale );
       
$this->set( 'url', $post_url );
       
$this->set( 'comment_status', $post_comment_status );
       
$this->set_renderers( $post_renderers );
       
$this->set( 'pst_ID', $item_st_ID );
       
$this->set( 'order', $postcat_order );

        if(
$this->get( 'ityp_ID' ) > 0 && isset( $this->custom_fields ) )
        {    
// Reinitialize custom fields definitions if they were created to set new values before set item type for this Item, e-g on install default Items:
           
$old_custom_fields = $this->custom_fields;
           
$this->custom_fields = $this->get_custom_fields_defs();
            foreach(
$this->custom_fields as $custom_field_name => $custom_field )
            {
                if( isset(
$old_custom_fields[ $custom_field_name ]['value'] ) )
                {
                   
$custom_field['value'] = $old_custom_fields[ $custom_field_name ]['value'];
                }
               
$this->custom_fields[ $custom_field_name ] = $custom_field;
            }
            unset(
$old_custom_fields );
        }

       
// Update the computed custom fields if this Item has them:
       
$custom_fields = $this->get_custom_fields_defs();
        foreach(
$custom_fields as $custom_field )
        {
            if(
$custom_field['type'] == 'computed' )
            {    
// Set a value by special function because we don't submit value for such fields and compute a value by formula automatically:
               
$this->set_custom_field( $custom_field['name'], $this->get_custom_field_computed( $custom_field['name'] ), 'value', true );
            }
        }

       
// INSERT INTO DB:
       
$this->dbinsert( $display_restrict_status_messages );

       
// Update post_datestart using FROM_UNIXTIME to prevent invalid datetime values during DST spring forward - fall back
       
$DB->query( 'UPDATE T_items__item SET post_datestart = FROM_UNIXTIME('.strtotime( $post_timestamp ).') WHERE post_ID = '.$DB->quote( $this->ID ) );

        return
$this->ID;
    }


   
/**
     * Insert object into DB based on previously recorded changes
     *
     * @param boolean Display restrict status messages
     * @return boolean true on success
     */
   
function dbinsert( $display_restrict_status_messages = true )
    {
        global
$DB, $current_User, $Plugins;

       
$DB->begin( 'SERIALIZABLE' );

        if( isset(
$this->previous_status ) )
        {    
// Restrict Item status by Collection access restriction AND by CURRENT USER write perm:
            // (ONLY if current request is updating item status)
           
$this->restrict_status( true, $display_restrict_status_messages );
        }

        if(
$this->status != 'draft' )
        {    
// The post is getting published in some form, set the publish date so it doesn't get auto updated in the future:
           
$this->set( 'dateset', 1 );
        }

        if( empty(
$this->creator_user_ID) )
        {
// No creator assigned yet, use current user:
           
$this->set_creator_User( $current_User );
        }

       
// Validate urltitle/slug:
       
$orig_urltitle = $this->urltitle;
       
$urltitles = explode( ',', $this->urltitle );
        foreach(
$urltitles as $u => $urltitle_value )
        {
           
$urltitles[ $u ] = utf8_trim( $urltitle_value );
        }
       
$orig_urltitle = implode( ',', array_unique( $urltitles ) );
       
$this->set( 'urltitle', urltitle_validate( $urltitles[0], $this->title, $this->ID, false, 'slug_title', 'slug_itm_ID', 'T_slug', $this->locale, 'T_items__item' ) );

       
$this->update_renderers_from_Plugins();

        if( isset(
$this->content_is_updated ) )
        {    
// Autogenerate new excerpt ONLY when content has been updated during current request:
           
$this->update_autogenerated_excerpt();
        }

        if( isset(
$Plugins) )
        {    
// Note: Plugins may not be available during maintenance, install or test cases
            // TODO: allow a plugin to cancel update here (by returning false)?
           
$Plugins->trigger_event( 'PrependItemInsertTransact', $params = array( 'Item' => & $this ) );
        }

       
$this->set_last_touched_ts();
       
$this->set_contents_last_updated_ts();

       
// Check if item is assigned to a user
       
if( isset( $this->dbchanges['post_assigned_user_ID'] ) )
        {
           
$this->assigned_to_new_user = true;
        }

       
$dbchanges = $this->dbchanges; // we'll save this for passing it to the plugin hook

       
if( $result = parent::dbinsert() )
        {
// We could insert the item object..

           
if( ! empty( $this->source_version_item_ID ) )
            {    
// Set group ID if this Item is creating as version of another Item:
               
$this->set_group_ID( $this->source_version_item_ID );
            }

           
// Link attachments from temporary object to new created Item:
           
$this->link_from_Request();

           
// Let's handle the extracats:
           
$result = $this->insert_update_extracats( 'insert' );

            if(
$result )
            {
// Let's handle the tags:
               
$this->insert_update_tags( 'insert' );
            }

           
// save Item settings
           
if( $result && isset( $this->ItemSettings ) && isset( $this->ItemSettings->cache[0] ) )
            {
               
// update item ID in the ItemSettings cache
               
$this->ItemSettings->cache[$this->ID] = $this->ItemSettings->cache[0];
                unset(
$this->ItemSettings->cache[0] );

               
$this->ItemSettings->dbupdate();
            }

           
// Update custom fields:
           
$this->update_custom_fields();

            if(
$result )
            {
               
modules_call_method( 'update_item_after_insert', array( 'edited_Item' => $this ) );
            }

           
// Let's handle the slugs:
           
$new_slugs = $this->update_slugs( $orig_urltitle );

            if(
$result && ! empty( $new_slugs ) )
            {    
// If we have new created slugs, we have to insert it into the database:
               
foreach( $new_slugs as $s => $new_Slug )
                {
                    if(
$new_Slug->ID == 0 )
                    {    
// Insert only new created slugs:
                       
if( ! $new_Slug->dbinsert() )
                        {
                           
$result = false;
                        }
                        elseif(
$s == 0 )
                        {
                           
$new_canonical_Slug = $new_slugs[0];
                        }
                    }
                }
               
// Clear cached slugs in order to display new unique updated on the edit form:
               
$this->slugs = NULL;
            }

           
// Create tiny slug:
           
$new_tiny_Slug = new Slug();
           
load_funcs( 'slugs/model/_slug.funcs.php' );
           
$tinyurl = getnext_tinyurl();
           
$new_tiny_Slug->set( 'title', $tinyurl );
           
$new_tiny_Slug->set( 'type', 'item' );
           
$new_tiny_Slug->set( 'itm_ID', $this->ID );

            if(
$result && ( $result = ( isset( $new_canonical_Slug ) && $new_tiny_Slug->dbinsert() ) ) )
            {
               
$this->set( 'canonical_slug_ID', $new_canonical_Slug->ID );
               
$this->set( 'tiny_slug_ID', $new_tiny_Slug->ID );
                if(
$result = parent::dbupdate() )
                {
                   
$DB->commit();

                   
// save the last tinyurl
                   
global $Settings;
                   
$Settings->set( 'tinyurl', $tinyurl );
                   
$Settings->dbupdate();

                    if( isset(
$Plugins) )
                    {    
// Note: Plugins may not be available during maintenance, install or test cases
                       
$Plugins->trigger_event( 'AfterItemInsert', $params = array( 'Item' => & $this, 'dbchanges' => $dbchanges ) );
                    }
                }
            }

           
// Update last touched date of this Item and also all categories of this Item
           
$this->update_last_touched_date( false, false );
        }

        if( !
$result )
        {
// Rollback current transaction
           
$DB->rollback();
        }

        return
$result;
    }


   
/**
     * Insert new item in test mode, Use this function only in test tool to create very much items at one time
     *
     * @return boolean true on success
     */
   
function dbinsert_test()
    {
        global
$DB, $localtimenow;

       
$this->set_param( 'last_touched_ts', 'date', date( 'Y-m-d H:i:s', $localtimenow ) );
       
$this->set_param( 'contents_last_updated_ts', 'date', date( 'Y-m-d H:i:s', $localtimenow ) );

       
$DB->begin( 'SERIALIZABLE' );

        if(
$result = parent::dbinsert() )
        {
// We could insert the item object..

           
if( ! is_null( $this->extra_cat_IDs ) )
            {
// Insert new extracats:
               
$query = 'INSERT INTO T_postcats ( postcat_post_ID, postcat_cat_ID ) VALUES ';
                foreach(
$this->extra_cat_IDs as $extra_cat_ID )
                {
                   
$query .= '( '.$this->ID.', '.$extra_cat_ID.' ),';
                }
               
$query = substr( $query, 0, strlen( $query ) - 1 );
               
$DB->query( $query, 'insert new extracats' );
            }

           
// Create canonical slug with urltitle
           
$canonical_Slug = new Slug();
           
$canonical_Slug->set( 'title', $this->urltitle );
           
$canonical_Slug->set( 'type', 'item' );
           
$canonical_Slug->set( 'itm_ID', $this->ID );

           
// Create tiny slug:
           
$tiny_Slug = new Slug();
           
load_funcs( 'slugs/model/_slug.funcs.php' );
           
$tinyurl = getnext_tinyurl();
           
$tiny_Slug->set( 'title', $tinyurl );
           
$tiny_Slug->set( 'type', 'item' );
           
$tiny_Slug->set( 'itm_ID', $this->ID );

            if(
$result = ( $canonical_Slug->dbinsert() && $tiny_Slug->dbinsert() ) )
            {
               
$this->set( 'canonical_slug_ID', $canonical_Slug->ID );
               
$this->set( 'tiny_slug_ID', $tiny_Slug->ID );
                if(
$result = parent::dbupdate() )
                {
// save the last tinyurl
                   
global $Settings;
                   
$Settings->set( 'tinyurl', $tinyurl );
                   
$Settings->dbupdate();
                }
            }
        }

        if(
$result )
        {
// The post and all related object was successfully created
           
$DB->commit();
        }
        else
        {
// Some error occured the transaction needs to be rollbacked
           
$DB->rollback();
        }

        return
$result;
    }


   
/**
     * Update the DB based on previously recorded changes
     *
     * @param boolean do we want to auto track the mod date?
     * @param boolean Update slug? - We want to PREVENT updating slug when item dbupdate is called,
     *     because of the item canonical url title was changed on the slugs edit form, so slug update is already done.
     *  If slug update wasn't done already, then this param has to be true.
     * @param boolean Update custom fields of child posts?
     * @param boolean|string TRUE - Force to create revision, FALSE - Auto create revision depending on last edit time, 'no' - Force to do NOT create revision
     * @return boolean true on success
     */
   
function dbupdate( $auto_track_modification = true, $update_slug = true, $update_child_custom_fields = true, $create_revision = false )
    {
        global
$DB, $Plugins, $Messages;

       
$DB->begin( 'SERIALIZABLE' );

        if( isset(
$this->previous_status ) )
        {    
// Restrict Item status by Collection access restriction AND by CURRENT USER write perm:
            // (ONLY if current request is updating item status)
           
$this->restrict_status( true );
        }

        if(
$this->status != 'draft' )
        {    
// The post is getting published in some form, set the publish date so it doesn't get auto updated in the future:
           
$this->set( 'dateset', 1 );
        }

       
// Check if item is assigned to a user
       
if( isset( $this->dbchanges['post_assigned_user_ID'] ) )
        {
           
$this->assigned_to_new_user = true;
        }

       
$dbchanges = $this->dbchanges; // we'll save this for passing it to the plugin hook

        // Check whether any db change has been executed
       
$db_changed = false;

       
// save Item settings
       
if( isset( $this->ItemSettings ) )
        {
            if(
$this->get_setting( 'last_import_hash' ) !== NULL &&
                ! isset(
$this->ItemSettings->changes['last_import_hash'] ) )
            {    
// Clear the setting if it is a manual updating and not import updating:
               
$this->delete_setting( 'last_import_hash' );
            }

           
$item_settings_changed = $this->ItemSettings->dbupdate();
           
$db_changed = $item_settings_changed || $db_changed;

            if(
$item_settings_changed )
            {    
// Update post modified date when at least one setting of this post updated, e.g. when custom field value has been updated:
               
global $localtimenow;
               
$this->set_param( $this->datemodified_field, 'date', date( 'Y-m-d H:i:s', $localtimenow ) );
            }
        }

       
// Update custom fields:
       
$db_changed = $this->update_custom_fields() || $db_changed;

        if(
$update_child_custom_fields )
        {    
// Update custom fields of all child posts of this post:
           
$custom_fields = $this->get_type_custom_fields();
            if( ! empty(
$custom_fields ) )
            {    
// If this post has at least one custom field
               
if( ! isset( $this->recursive_updated_items ) )
                {    
// Store in this array all updated items in order to avoid inifitie loop updating:
                   
$this->recursive_updated_items = array();
                   
$this->recursive_updated_messages = array();
                }
               
$this->recursive_updated_items[] = $this->ID;

               
$ItemCache = & get_ItemCache();
               
$ItemCache->clear();
               
$item_cache_SQL = $ItemCache->get_SQL_object();
               
$item_cache_SQL->FROM_add( 'INNER JOIN T_items__type ON ityp_ID = post_ityp_ID' );
               
$item_cache_SQL->WHERE_and( 'post_parent_ID = '.$this->ID );
               
$item_cache_SQL->WHERE_and( 'ityp_use_parent != "never"' );
               
$child_items = $ItemCache->load_by_sql( $item_cache_SQL );
                foreach(
$child_items as $child_Item )
                {
                    if(
in_array( $child_Item->ID, $this->recursive_updated_items ) )
                    {    
// Display error to inform about infinite loop:
                       
if( ! isset( $this->recursive_updated_messages['nogroup'] ) )
                        {
                           
$this->recursive_updated_messages['nogroup'] = array();
                        }
                       
$this->recursive_updated_messages['nogroup'][] = array(
                           
'text' => sprintf( T_('Recursive update has stopped because of infinite loop. Item #%d has child #%d which was already updated.'), intval( $this->ID ), intval( $child_Item->ID ) ),
                           
'type' => 'error',
                        );
                       
// Stop here to avoid infinite loop:
                       
continue;
                    }
                   
$child_custom_fields = $child_Item->get_custom_fields_defs();
                    if( ! empty(
$child_custom_fields ) )
                    {    
// If child post has at least one custom field:
                       
$update_child_custom_field = false;
                        foreach(
$custom_fields as $custom_field_code => $custom_field )
                        {
                            if( isset(
$child_custom_fields[ $custom_field_code ] ) &&
                               
$child_custom_fields[ $custom_field_code ]['type'] == $custom_field['type'] &&
                               
$child_custom_fields[ $custom_field_code ]['parent_sync'] &&
                               
$custom_field['type'] != 'computed' ) // NOTE: we must NOT copy the computed values from parent because child custom field may has a different formula!
                           
{    // If child post has a custom field with same code and type:
                               
$custom_field_make_null = $custom_field['type'] != 'double'; // store '0' values in DB for numeric fields
                               
$child_Item->set_custom_field( $custom_field['name'], $this->get_custom_field_value( $custom_field_code, $custom_field['type'] ), 'value', $custom_field_make_null );
                               
// Mark to know custom fields of the child post must be updated from parent:
                               
$update_child_custom_field = true;
                            }
                        }
                       
// NOTE: we must recompute values of the "computed" fields because child custom field may has a different formula than parent!
                       
foreach( $child_custom_fields as $child_custom_field )
                        {    
// Update computed custom fields after when all fields we updated above:
                           
if( $child_custom_field['type'] == 'computed' )
                            {    
// Set a value by special function because we don't submit value for such fields and compute a value by formula automatically:
                               
$child_Item->set_custom_field( $child_custom_field['name'], $child_Item->get_custom_field_computed( $child_custom_field['name'] ) );
                               
// Mark to know custom fields of the child post must be updated from parent:
                               
$update_child_custom_field = true;
                            }
                        }
                        if(
$update_child_custom_field )
                        {    
// Update child post custom fields if at least one field has been detected with same code and type as parent:
                           
$child_Item->recursive_updated_items = & $this->recursive_updated_items;
                           
$child_Item->recursive_updated_messages = & $this->recursive_updated_messages;
                            if(
$child_Item->dbupdate() )
                            {    
// Display a message to inform about updated child posts:
                               
$child_item_Blog = $child_Item->get_Blog();
                               
$child_item_edit_link = $child_Item->get_edit_link( array(
                                       
'text'   => $child_Item->ID,
                                       
'before' => '',
                                       
'after'  => '',
                                    ) );
                                if( !
$child_item_edit_link )
                                {    
// If current user has no permission to edit the child Item display the ID as text:
                                   
$child_item_edit_link = $child_Item->ID;
                                }
                               
$msg_group = T_('Custom fields have been replicated to the following child posts').':';
                                if( ! isset(
$this->recursive_updated_messages[ $msg_group ] ) )
                                {
                                   
$this->recursive_updated_messages[ $msg_group ] = array();
                                }
                               
$this->recursive_updated_messages[ $msg_group ][] = array(
                                   
'text' => $child_Item->get_title().' ('.$child_item_edit_link.') '.T_('in').' '.$child_item_Blog->get( 'shortname' ),
                                   
'type' => 'note',
                                );
                            }
                        }
                    }
                }

               
// Display messages from recursion:
               
if( $this->ID == $this->recursive_updated_items[0] &&
                    ! empty(
$this->recursive_updated_messages ) )
                {    
// If we have at least one message during recursive updating of the child posts and this is end of the recursion:
                   
foreach( $this->recursive_updated_messages as $msg_group => $messages )
                    {    
// Reverse message to display in proper way and not as it is returned by recursion:
                       
$messages = array_reverse( $messages );
                        foreach(
$messages as $message )
                        {
                            if(
$msg_group == 'nogroup' )
                            {    
// Single message:
                               
$Messages->add( $message['text'], $message['type'] );
                            }
                            else
                            {    
// Grouped message:
                               
$Messages->add_to_group( $message['text'], $message['type'], $msg_group );
                            }
                        }
                    }
                    unset(
$this->recursive_updated_items );
                    unset(
$this->recursive_updated_messages );
                }
            }
        }

       
// validate url title / slug
       
if( $update_slug )
        {
// item canonical slug wasn't updated outside from this call, if it was changed or it wasn't set yet, we must update the slugs
           
if( empty( $this->urltitle ) || isset( $this->dbchanges['post_urltitle'] ) )
            {
// Url title has changed or is empty, we do need to update the slug:
               
$edited_slugs = $this->update_slugs();
               
$db_changed = true;
            }
        }

       
$db_changed = $this->update_renderers_from_Plugins() || $db_changed;

        if( isset(
$this->content_is_updated ) )
        {    
// Autogenerate new excerpt ONLY when content has been updated during current request:
           
$this->update_autogenerated_excerpt();
        }

       
// TODO: dh> allow a plugin to cancel update here (by returning false)?
       
$Plugins->trigger_event( 'PrependItemUpdateTransact', $params = array( 'Item' => & $this ) );

       
$result = true;
       
// fp> note that dbchanges isn't actually 100% accurate. At this time it does include variables that actually haven't changed.
       
if( isset( $this->dbchanges['post_status'] )
            || isset(
$this->dbchanges['post_title'] )
            || isset(
$this->dbchanges['post_content'] )
            || isset(
$this->dbchanges_flags['custom_fields'] ) )
        {
// One of the fields we track in the revision history has changed:
            // Save the "current" (soon to be "old") data as a version before overwriting it in parent::dbupdate:
            // fp> TODO: actually, only the fields that have been changed should be copied to the version, the other should be left as NULL

           
global $localtimenow;
            if(
$create_revision !== 'no' && ( $create_revision || $localtimenow - strtotime( $this->last_touched_ts ) > 10 ) )
            {    
// Create new revision:
               
$result = $this->create_revision();
            }

           
$db_changed = true;
        }

        if(
$auto_track_modification && ( count( $dbchanges ) > 0 || ! empty( $this->dbchanges_custom_fields ) ) )
        {
            if( ! isset(
$dbchanges['last_touched_ts'] ) )
            {    
// Update last_touched_ts field only if it wasn't updated yet and the datemodified will be updated for sure:
               
$this->set_last_touched_ts();
            }
            if( ! isset(
$dbchanges['contents_last_updated_ts'] ) &&
              ( isset(
$dbchanges['post_title'] ) ||
                isset(
$dbchanges['post_content'] ) ||
                isset(
$dbchanges['post_url'] ) ) )
            {    
// If at least one of those fields has been updated then it means a content of this item has been updated:
               
$this->set_contents_last_updated_ts();
            }
        }

       
// Unset the following otherwise a new revision will be created if another dbupdate is called
       
$this->dbchanges_custom_fields = array();
        unset(
$this->dbchanges_flags['custom_fields'] );

       
// Let's handle the slugs:
        // TODO: dh> $result handling here feels wrong: when it's true already, it should not become false (add "|| $result"?)
        // asimo>dh The result handling is in a transaction. If somehow the new slug creation fails, then the item insertion should rollback as well
       
if( $result && ! empty( $edited_slugs ) )
        {    
// if we have new created $edited_slugs, we have to insert it into the database:
           
foreach( $edited_slugs as $edited_Slug )
            {
                if(
$edited_Slug->ID == 0 )
                {    
// Insert only new created slugs:
                   
$edited_Slug->dbinsert();
                }
            }
            if( isset(
$edited_slugs[0] ) && $edited_slugs[0]->ID > 0 )
            {    
// Make first slug from list as main slug for this item:
               
$this->set( 'canonical_slug_ID', $edited_slugs[0]->ID );
               
$this->set( 'urltitle', $edited_slugs[0]->get( 'title' ) );
            }
           
// Clear cached slugs in order to display new unique updated on the edit form:
           
$this->slugs = NULL;
        }

       
$parent_update = $this->dbupdate_worker( $auto_track_modification );
        if(
$result && ( $parent_update !== false ) )
        {
// We could update the item object:
           
$db_changed = $db_changed || ( $parent_update !== NULL );

            if( isset(
$this->dbchanges_flags['extra_cat_IDs'] ) )
            {
// Let's handle the extracats:
               
$result = $this->insert_update_extracats( 'update' );
               
$db_changed = true;
            }

            if(
$result && isset( $this->dbchanges_flags['tags'] ) )
            {
// Let's handle the tags:
               
$this->insert_update_tags( 'update' );
               
$db_changed = true;
            }

           
// Update last touched date of this Item and also all categories of this Item
           
$this->update_last_touched_date( false, false );
        }

       
// Check if there were failed nested transaction
       
$result = $result && ( ! $DB->has_failed_transaction() );
        if(
$result === false )
        {
// Update failed
           
$DB->rollback();
           
$db_changed = false;
        }
        else
        {
// Update was successful
           
if( $db_changed && ! empty( $dbchanges ) )
            {    
// There were some db modification for item's content and related settings
                // (Don't clear prerendered cache and comments when for example only extra cats or tags were updated (see $this->dbchanges_flags) )

                // Delete prerendered content:
               
$this->delete_prerendered_content();

               
// Update comments of this Item:
               
$this->update_comments();
            }

           
$DB->commit();

            if( empty(
$this->AfterItemUpdate_is_executed ) )
            {    
// Execute this event once per request:
               
$Plugins->trigger_event( 'AfterItemUpdate', $params = array( 'Item' => & $this, 'dbchanges' => $dbchanges ) );
               
// Set flag to know we have already executed this plugin event:
               
$this->AfterItemUpdate_is_executed = true;
            }
        }

        if(
$db_changed )
        {
// There were db modificaitons, needs cache invalidation
            // Load the blog we're in:
           
$Collection = $Blog = & $this->get_Blog();

            if(
$this->get_type_setting( 'usage' ) == 'content-block' &&
                empty(
$this->content_block_invalidate_reported ) )
            {    
// Display warning on updating of content block item:
               
global $admin_url;

               
// Get items where currently updated content block is included:
               
$invalidated_items = $this->get_included_item_IDs( $this->ID.'|'.$this->get_slugs( '|' ) );
               
$invalidated_items_num = count( $invalidated_items );
                if(
$invalidated_items_num > 0 )
                {    
// Delete pre-rendered cache of the found items:
                   
$invalidated_items_num = $DB->query( 'DELETE FROM T_items__prerendering
                        WHERE itpr_itm_ID IN ( '
.$DB->quote( $invalidated_items ).' )',
                       
'Delete pre-rendered cache on updating content-block Item #'.$this->ID );
                }

               
// Display info message about invalidated cache:
               
$invalidate_message = TB_('INFO: you edited a content block.').' '
                   
.sprintf( TB_('We invalidated %d pre-rendered Items that include the content block.'), $invalidated_items_num ).' ';
                if(
check_user_perm( 'admin', 'normal' ) &&
                   
check_user_perm( 'options', 'view' ) )
                {    
// If current user has a permission to the clear tool:
                   
$Messages->add( $invalidate_message.sprintf( TB_('You may <a %s>invalidate the <b>complete</b> pre-rendering cache NOW</a>.'), 'href="'.$admin_url.'?ctrl=tools&amp;action=del_itemprecache&amp;'.url_crumb( 'tools' ).'" target="_blank"' ), 'note' );
                }
                else
                {    
// If current user has no permission to the clear tool:
                   
$Messages->add( $invalidate_message.TB_('Please ask administrator to invalidate the pre-rendering cache.'), 'note' );
                }

               
$this->content_block_invalidate_reported = true;
            }

           
// BLOCK CACHE INVALIDATION:
           
BlockCache::invalidate_key( 'cont_coll_ID', $Blog->ID ); // Content has changed
           
BlockCache::invalidate_key( 'item_ID', $this->ID ); // Item has changed
           
BlockCache::invalidate_key( 'item_'.$this->ID, 1 ); // Item has changed (useful for compare widget which needs to check several item_IDs, including from different collections)

           
if( $this->is_intro() || $this->is_featured() )
            {
// Content of intro or featured post has changed
               
BlockCache::invalidate_key( 'intro_feat_coll_ID', $Blog->ID );
            }
        }

       
// set_coll_ID // Settings have not changed

       
return $result;
    }


   
/**
     * Get IDs of items where this content-block is included
     * Used to invalidate pre-rendered content
     *
     * @param string Slugs separated by |
     * @return array
     */
   
function get_included_item_IDs( $slugs )
    {
        global
$DB;

       
$slugs = trim( $slugs, '|' );
        if(
$slugs === '' )
        {    
// Wrong request without slugs:
           
return array();
        }

       
// Get items where currently updated content block is included:
       
$SQL = new SQL( 'Get items with included Item #'.$this->ID.' in order to invalidate pre-rendered content' );
       
$SQL->SELECT( 'post_ID, ityp_usage, IF( ityp_usage = "content-block", GROUP_CONCAT( slug_title SEPARATOR "|" ), NULL ) AS slugs' );
       
$SQL->FROM( 'T_items__item' );
       
$SQL->FROM_add( 'INNER JOIN T_items__prerendering ON post_ID = itpr_itm_ID' );
       
$SQL->FROM_add( 'INNER JOIN T_items__type ON post_ityp_ID = ityp_ID' );
       
$SQL->FROM_add( 'INNER JOIN T_slug ON post_ID = slug_itm_ID AND slug_ID != post_tiny_slug_ID' );
       
$SQL->WHERE( 'post_content REGEXP '.$DB->quote( '\[(include|cblock):('.$slugs.')(:[^]]+)?\]' ) );
       
$SQL->GROUP_BY( 'post_ID' );
       
$content_items = $DB->get_results( $SQL );

       
$included_items = array();
        foreach(
$content_items as $content_item )
        {
           
$included_items[] = $content_item->post_ID;
            if(
$content_item->ityp_usage == 'content-block' &&
                ! empty(
$content_item->slugs ) )
            {    
// Try to find recursively where the content-block Item is included yet:
               
$block_items = $this->get_included_item_IDs( $content_item->post_ID.'|'.$content_item->slugs );
               
$included_items = array_merge( $included_items, $block_items );
            }
        }

        return
array_unique( $included_items );
    }


   
/**
     * Create new slugs with validated title
     * !!!private!!! This function should be called only from Item dbupdate() function
     * @private
     * @return array Slug objects
     */
   
function update_slugs( $urltitle = NULL )
    {
       
$SlugCache = & get_SlugCache();

        if( ! isset(
$urltitle ) )
        {
           
$urltitle = $this->urltitle;
        }

       
// Split slugs by comma
       
$urltitles = explode( ',', $urltitle );

       
$edited_slugs = array();
        foreach(
$urltitles as $urltitle )
        {
           
$urltitle = trim( $urltitle );

           
// create new slug
           
$new_Slug = new Slug();
           
// urltitle_validate may modify the urltitle !!!
           
$new_Slug->set( 'title', urltitle_validate( $urltitle, $this->title, $this->ID, false, $new_Slug->dbprefix.'title', $new_Slug->dbprefix.'itm_ID', $new_Slug->dbtablename, $this->locale ) );
           
$new_Slug->set( 'type', 'item' );
           
$new_Slug->set( 'itm_ID', $this->ID );

            if( (
$urltitle != $new_Slug->get( 'title' ) ) &&
                (
strtolower( $urltitle ) == $new_Slug->get( 'title' ) ) &&
                (
$prev_Slug = $SlugCache->get_by_name( $urltitle, false, false ) ) )
            {    
// Allow to use uppercase chars in slug title only if this is a single difference between requested slug title and result of urltitle_validate(),
                // and only if such slug title alredy exists in DB:
                // (such case possible after item merging when all sulgs were merged and also tiny slugs which have a format like aC8)
               
$new_Slug->set( 'title', $urltitle );
            }

           
// Check if this slug was already used by this item or not.
            // We need this check, because urltitle_validate() function will modify an existing urltitle only if it belongs to a different object
           
$prev_Slug = $SlugCache->get_by_name( $new_Slug->get('title'), false, false );
            if(
$prev_Slug )
            {
// A slug with this title already exists. It must belong to the same item!
               
if( $prev_Slug->get('itm_ID') == $new_Slug->get('itm_ID') )
                {
                   
$edited_slugs[] = $prev_Slug;
                    continue;
                }
                else
                {
// This case should never happen, because urltitle validate check this case. It is only an extra check.
                   
debug_die('The slugs table is broken');
                }
            }
            else
            {
// No slug with such urltitle in DB, we can add this new one
               
$edited_slugs[] = $new_Slug;
            }
        }

        return
$edited_slugs;
    }


   
/**
     * Update comments of this Item
     */
   
function update_comments()
    {
        global
$DB;

        if( empty(
$this->ID ) )
        {    
// This function can works only with existing Item:
           
return;
        }

        if( isset(
$this->previous_status ) )
        {    
// Restrict comments status by this Item status if it has been changed:
           
$max_allowed_comment_status = $this->get( 'status' );
            if(
$max_allowed_comment_status == 'redirected' )
            {    
// Comments cannot have a status "Redirected", so reduce them only to "Deprecated":
               
$max_allowed_comment_status = 'deprecated';
            }

           
$ordered_statuses = get_visibility_statuses( 'ordered-index' );
           
$reduce_comment_status = false;
           
$reduced_statuses = array();
            foreach(
$ordered_statuses as $status_key => $status_order )
            {
                if(
$status_key == $max_allowed_comment_status )
                {    
// This status is max allowed for item's comments, Reduce all next higher statuses:
                   
$reduce_comment_status = true;
                    continue;
                }
                if(
$reduce_comment_status )
                {    
// This comment status must be reduced to current status of this Item:
                   
$reduced_statuses[] = $status_key;
                }
            }

            if( ! empty(
$reduced_statuses ) )
            {    
// Reduce statuses of item's comments to current status of this Item:
               
$DB->query( 'UPDATE T_comments
                      SET comment_status = '
.$DB->quote( $max_allowed_comment_status ).'
                    WHERE comment_item_ID = '
.$this->ID.'
                      AND comment_status IN ( '
.$DB->quote( $reduced_statuses ).' )',
                   
'Reduce comments statutes to status of Item #'.$this->ID );
            }
        }
    }


   
/**
     * Trigger event AfterItemDelete after calling parent method.
     *
     * @todo fp> delete related stuff: comments, cats, file links...
     *
     * @return boolean true on success
     */
   
function dbdelete()
    {
        global
$DB, $Plugins;

       
// remember ID, because parent method resets it to 0
       
$old_ID = $this->ID;

       
// Load the blog
       
$Collection = $Blog = & $this->get_Blog();

       
$DB->begin();

        if(
$r = parent::dbdelete() )
        {
           
// re-set the ID for the Plugin event & for a deleting of the prerendered content
           
$this->ID = $old_ID;

           
$DB->commit();

           
$Plugins->trigger_event( 'AfterItemDelete', $params = array( 'Item' => & $this ) );

           
$this->ID = 0;

           
// BLOCK CACHE INVALIDATION:
           
BlockCache::invalidate_key( 'cont_coll_ID', $Blog->ID ); // Content has changed
           
BlockCache::invalidate_key( 'item_ID', $old_ID ); // Item has deleted
           
BlockCache::invalidate_key( 'item_'.$old_ID, 1 ); // Item has deleted (useful for compare widget which needs to check several item_IDs, including from different collections)

           
if( $this->is_intro() || $this->is_featured() )
            {
// Content of intro or featured post has changed
               
BlockCache::invalidate_key( 'intro_feat_coll_ID', $Blog->ID );
            }

           
// set_coll_ID // Settings have not changed
       
}
        else
        {
           
$DB->rollback();
        }

        return
$r;
    }


   
/**
     * Update excerpt but ONLY if it is autogenerated
     *
     * This can be executed if $this->set( 'content', ... ) has been called before
     */
   
function update_autogenerated_excerpt()
    {
        if(
$this->get( 'excerpt_autogenerated' ) )
        {    
// We want to auto-generate excerpts for this Item:
           
if( ! empty( $this->content_is_updated ) )
            {    
// Clear prerendered content to generate excerpt from new content because the content has been really changed during current request:
               
$this->delete_prerendered_content();
            }

           
$this->set_param( 'excerpt', 'string', $this->get_autogenerated_excerpt() );
        }
    }


   
/**
     * Get autogenerated excerpt, derived from {@link Item::$content}.
     *
     * @param int Maximum length
     * @param string Tail to use, when string gets cropped. Its length gets
     *               substracted from the total length (with HTML entities
     *               being decoded). Default is "&hellip;" (HTML entity)
     * @return string
     */
// fp>yura: please check why this function is very different from get_content_excerpt() and use best code for both
   
function get_autogenerated_excerpt( $maxlen = 254, $tail = '&hellip;' )
    {
       
// Autogenerated excerpts should NEVER show anything after [teaserbreak] or after [pagebreak]
       
$content_parts = $this->get_content_parts( array( 'disppage' => 1 ) );
       
$first_content_part = array_shift( $content_parts );

       
// Render inline tags to HTML code, except of inline file tags because they are removed below:
       
$first_content_part = $this->render_inline_tags( $first_content_part, array(
               
'render_tag_image'             => false,
               
'render_tag_file'              => false,
               
'render_tag_inline'            => false,
               
'render_tag_video'             => false,
               
'render_tag_audio'             => false,
               
'render_tag_thumbnail'         => false,
               
'render_tag_folder'            => false,
               
'render_tag_item_link'         => false,
               
'render_tag_item_field'        => false,
               
'render_tag_item_titlelink'    => false,
               
'render_tag_item_url'          => false,
               
'render_tag_item_subscribe'    => false,
               
'render_tag_item_emailcapture' => false,
               
'render_tag_item_compare'      => false,
               
'render_tag_item_fields'       => false,
               
'render_tag_switcher'          => false,
               
'render_tag_switchable_div'    => false,
            ) );

       
// Remove shorttags from excerpt // [image:123:caption:.class] [file:123:caption:.class] [inline:123:.class] etc:
       
$first_content_part = preg_replace( '/\[[a-z]+:[^\]`]*\]/i', '', $first_content_part );

       
// Clean up rendering errors from autogenerated excerpt:
       
$first_content_part = clear_rendering_errors( $first_content_part );

        return
excerpt( $first_content_part, $maxlen, $tail );
    }


   
/**
     * Insert/Update post extracats
     *
     * @param string 'insert' | 'update'
     * @return boolean true on success | false one failure
     */
   
function insert_update_extracats( $mode )
    {
        global
$DB, $Messages, $Settings, $Blog;

        if( !
is_null( $this->extra_cat_IDs ) )
        {
// Okay the extra cats are defined:
           
$DB->begin( 'SERIALIZABLE' );

           
$meta_count = $DB->get_var( 'SELECT count( cat_ID ) FROM T_categories WHERE cat_meta = 1 AND cat_ID IN ('.implode( ',', $this->extra_cat_IDs ).')' );
            if(
$meta_count > 0 )
            {
               
$DB->rollback();
               
$Messages->add( T_('Could not set the selected categories!'), 'error' );
                return
false;
            }

           
// Load item orders per categories before delete them from DB:
           
$this->load_orders();

            if(
$mode == 'update' )
            {
               
// delete previous extracats:
               
$DB->query( 'DELETE FROM T_postcats WHERE postcat_post_ID = '.$this->ID, 'Delete previous extracats of Item #'.$this->ID );
            }

           
// Allow to use field "postcat_order" only since 12972 DB version:
           
$postcat_order_field = ( $Settings->get( 'db_version' ) >= 12972 ? ', postcat_order' : '' );

           
// insert new extracats:
           
$query = 'INSERT INTO T_postcats ( postcat_post_ID, postcat_cat_ID'.$postcat_order_field.' ) VALUES ';
            foreach(
$this->extra_cat_IDs as $extra_cat_ID )
            {
               
$query .= '( '.$this->ID.', '.$extra_cat_ID;
                if( ! empty(
$postcat_order_field ) )
                {    
// Insert item order per category only when this field exists in DB:
                   
$query .= ', '.$DB->quote( $this->get_order( $extra_cat_ID ) );
                }
               
$query .= ' ),';
            }
           
$query = substr( $query, 0, strlen( $query ) - 1 );
           
$DB->query( $query, 'Insert new extracats for Item #'.$this->ID );

           
$DB->commit();
        }

        return
true;
    }


   
/**
     * Save tags to DB
     *
     * @param string 'insert' | 'update'
     */
   
function insert_update_tags( $mode )
    {
        global
$DB;

        if( isset(
$this->tags ) )
        {
// Okay the tags are defined:

           
$DB->begin();

            if(
$mode == 'update' )
            {    
// delete previous tag associations:
                // Note: actual tags never get deleted
               
$DB->query( 'DELETE FROM T_items__itemtag
                                            WHERE itag_itm_ID = '
.$this->ID, 'delete previous tags' );
            }

            if( ! empty(
$this->tags ) )
            {
               
// Find the tags that are already in the DB
               
$query = 'SELECT tag_name
                                        FROM T_items__tag
                                     WHERE tag_name IN ('
.$DB->quote( $this->tags ).')';
               
$existing_tags = $DB->get_col( $query, 0, 'Find existing tags' );

               
$new_tags = array_diff( $this->tags, $existing_tags );

                if( !empty(
$new_tags ) )
                {
// insert new tags:
                   
$query = "INSERT INTO T_items__tag( tag_name ) VALUES ";
                    foreach(
$new_tags as $tag )
                    {
                       
$query .= '( '.$DB->quote($tag).' ),';
                    }
                   
$query = substr( $query, 0, strlen( $query ) - 1 );
                   
$DB->query( $query, 'insert new tags' );
                }

               
// ASSOC:
               
$query = 'INSERT INTO T_items__itemtag( itag_itm_ID, itag_tag_ID )
                                  SELECT '
.$this->ID.', tag_ID
                                      FROM T_items__tag
                                     WHERE tag_name IN ('
.$DB->quote( $this->tags ).')';
               
$DB->query( $query, 'Make tag associations!' );
            }

           
$DB->commit();
        }
    }


   
/**
     * Get the User who is assigned to the Item.
     *
     * @return User|NULL NULL if no user is assigned.
     */
   
function get_assigned_User()
    {
        if( ! isset(
$this->assigned_User) && isset($this->assigned_user_ID) )
        {
           
$UserCache = & get_UserCache();
           
$this->assigned_User = & $UserCache->get_by_ID( $this->assigned_user_ID );
        }

        return
$this->assigned_User;
    }


   
/**
     * Get the User who edited the Item last time.
     *
     * @return User
     */
   
function & get_lastedit_User()
    {
        if(
is_null( $this->lastedit_User ) )
        {
           
$UserCache = & get_UserCache();
           
$this->lastedit_User = & $UserCache->get_by_ID( $this->lastedit_user_ID, false, false );
        }

        return
$this->lastedit_User;
    }


   
/**
     * Get the User who created the Item.
     *
     * @return User
     */
   
function & get_creator_User()
    {
        if(
is_null($this->creator_User) )
        {
           
$UserCache = & get_UserCache();
           
$this->creator_User = & $UserCache->get_by_ID( $this->creator_user_ID );
           
$this->Author = & $this->creator_User;  // deprecated
       
}

        return
$this->creator_User;
    }


   
/**
     * Get login of the User who created the Item.
     *
     * @return string login
     */
   
function get_creator_login()
    {
       
$this->get_creator_User();
        if(
is_null( $this->creator_user_login ) && !is_null( $this->creator_User ) )
        {
           
$this->creator_user_login = $this->creator_User->login;
        }
        return
$this->creator_user_login;
    }


   
/**
     * Execute or schedule various notifications:
     * - notifications for moderators
     * - notifications for subscribers
     * - pings
     *
     * @param integer User ID who executed the action which will be notified, or NULL if it was executed by current logged in User
     * @param boolean TRUE if it is notification about new item, FALSE - for edited item
     * @param boolean|string Force sending notifications for members:
     *                       false   - Auto mode depending on current item statuses
     *                       'skip'  - Skip notifications
     *                       'force' - Force notifications
     *                       'mark'  - Change DB flag to "notified/sent" but do NOT actually send notifications
     * @param boolean|string Force sending notifications for community (use same values of second param)
     * @param boolean|string Force sending outbound pings (use same values of second param)
     * @return boolean TRUE on success
     */
   
function handle_notifications( $executed_by_userid = NULL, $is_new_item = false, $force_members = false, $force_community = false, $force_pings = false )
    {
        global
$Settings, $Messages, $localtimenow, $Debuglog, $DB;

        if( empty(
$this->ID ) )
        {    
// Don't send notifications for not created Item:
           
$Debuglog->add( 'Item->handle_notifications() : Item is NOT saved in DB', 'notifications' );
            return
false;
        }

       
// Immediate notifications? Asynchronous? Off?
       
$notifications_mode = $Settings->get( 'outbound_notifications_mode' );

        if(
$notifications_mode == 'off' )
        {    
// Don't send any notifications nor pings:
           
$Debuglog->add( 'Item->handle_notifications() : Notifications are turned OFF!', 'notifications' );
            return
false;
        }

        if(
$executed_by_userid === NULL && is_logged_in() )
        {    
// Use current user by default:
           
global $current_User;
           
$executed_by_userid = $current_User->ID;
        }

       
// FIRST: Moderators need to be notified immediately, even if the post is a draft/review and/or has an issue_date in the future.
        // fp> NOTE: for simplicity, for now, we will NOT make a scheduled job for this (but we will probably do so in the future)
       
$Debuglog->add( 'Item->handle_notifications() : Moderator notifications will always be immediate (never scheduled)', 'notifications' );
       
// Send email notifications to users who can moderate this item:
       
$already_notified_user_IDs = $this->send_moderation_emails( $executed_by_userid, $is_new_item );

       
// SECOND: Send email notification to assigned user
       
$Blog = & $this->get_Blog();
        if(
$Blog->get_setting( 'use_workflow' ) && $this->assigned_to_new_user && ! empty( $this->assigned_user_ID ) )
        {
           
$already_notified_user_IDs = array_merge( $already_notified_user_IDs, $this->send_assignment_notification( $executed_by_userid ) );
        }

       
// THIRD: Subscribers may be notified asynchornously... and that is a even a requirement if the post has an issue_date in the future.

       
$notified_flags = array();
        if(
$force_members == 'mark' )
        {    
// Only change DB flag to "members_notified" but do NOT actually send notifications:
           
$force_members = false;
           
$notified_flags[] = 'members_notified';
           
$this->display_notification_message( T_('Marking email notifications for members as sent.') );
        }
        if(
$force_community == 'mark' )
        {    
// Only change DB flag to "community_notified" but do NOT actually send notifications:
           
$force_community = false;
           
$notified_flags[] = 'community_notified';
           
$this->display_notification_message( T_('Marking email notifications for community as sent.') );
        }
        if(
$force_pings == 'mark' )
        {    
// Only change DB flag to "pings_sent" but do NOT actually send pings:
           
$force_pings = false;
           
$notified_flags[] = 'pings_sent';
           
$this->display_notification_message( T_('Marking pings as sent.') );
        }
        if( ! empty(
$notified_flags ) )
        {    
// Save the marked processing status to DB:
           
$this->set( 'notifications_flags', $notified_flags );
           
$this->dbupdate( false, false, false );
        }

       
// Instead of the above we now check the flags:
       
if( ( $force_members != 'force' && $force_community != 'force' && $force_pings != 'force' ) &&
           
$this->check_notifications_flags( array( 'members_notified', 'community_notified', 'pings_sent' ) ) )
        {    
// All possible notifications have already been sent and no forcing for any notification:
           
$this->display_notification_message( T_('All possible notifications have already been sent: skipping notifications...') );
           
$Debuglog->add( 'Item->handle_notifications() : All possible notifications have already been sent: skipping notifications...', 'notifications' );
            return
false;
        }

       
// IMMEDIATE vs ASYNCHRONOUS sending:
       
$DB->begin( 'SERIALIZABLE' );

        if(
$notifications_mode == 'immediate' && strtotime( $this->issue_date ) <= $localtimenow )
        {    
// We want to send the notifications immediately (can only be done if post does not have an issue_date in the future):

           
$Debuglog->add( 'Item->handle_notifications() : Sending immediate Pings & Subscriber notifications', 'notifications' );

           
// Send outbound pings: (will only do something if visibility is 'public')
           
$this->send_outbound_pings( $force_pings );

           
// Send email notifications to users who want to receive them for the collection of this item: (will be different recipients depending on visibility)
           
$notified_flags = $this->send_email_notifications( $executed_by_userid, $is_new_item, $already_notified_user_IDs, $force_members, $force_community );

           
// Record that we have just notified the members and/or community:
           
$this->set( 'notifications_flags', $notified_flags );

           
// Record that processing has been done:
           
$this->set( 'notifications_status', 'finished' );
        }
        elseif(
$this->get( 'notifications_status' ) != 'todo' && $this->get( 'notifications_status' ) != 'started' )
        {    
// We want asynchronous post processing. (This automatically applies to posts with issue_date in the future):

           
if( $notifications_mode == 'immediate' )
            {    
// We ended up here because the issue_date is in the future BUT notifications are not sent to asynchronoys...
                // This means we will schedule a job but it will never get executed until the admin turns on async notifications:
               
$Messages->add( sprintf( T_('You just published a post in the future. You must set your notifications to <a %s>Asynchronous</a> so that b2evolution can send out notification when this post goes live.'),
                   
'href="http://b2evolution.net/man/after-each-post-settings" target="_blank"' ), 'warning' );
            }

           
// CREATE CRON JOB OBJECT:

            // Note: in case of successive edits of a post we may create many cron jobs for it.
            // It will be the responsibility of the cron jobs to detect if another one is already running and not execute twice or more times concurrently.

           
$Debuglog->add( 'Item->handle_notifications() : Scheduling notifications through a cron job', 'notifications' );

           
load_class( '/cron/model/_cronjob.class.php', 'Cronjob' );
           
$item_Cronjob = new Cronjob();

           
// start datetime. We do not want to ping before the post is effectively published:
           
$item_Cronjob->set( 'start_datetime', $this->issue_date );

           
// no repeat.

            // key:
           
$item_Cronjob->set( 'key', 'send-post-notifications' );

           
// params: specify which post this job is supposed to send notifications for:
           
$item_Cronjob->set( 'params', array(
                   
'item_ID'                   => $this->ID,
                   
'executed_by_userid'        => $executed_by_userid,
                   
'is_new_item'               => $is_new_item,
                   
'already_notified_user_IDs' => $already_notified_user_IDs,
                   
'force_members'             => $force_members,
                   
'force_community'           => $force_community,
                   
'force_pings'               => $force_pings,
                ) );

           
// Save cronjob to DB:
           
if( $item_Cronjob->dbinsert() )
            {
               
$this->display_notification_message( T_('Scheduling Pings & Subscriber email notifications.') );

               
// Memorize the cron job ID which is going to handle this post:
               
$this->set( 'notifications_ctsk_ID', $item_Cronjob->ID );

               
// Record that processing has been scheduled:
               
$this->set( 'notifications_status', 'todo' );
            }
        }

       
// Save the new processing status to DB, but do not update last edited by user, slug or child custom fields:
       
if( $this->dbupdate( false, false, false ) )
        {
           
$DB->commit();
            return
true;
        }
        else
        {
           
$DB->rollback();
            return
false;
        }
    }


   
/**
     * Get item moderators
     *
     * @param string Setting name of notification: 'notify_post_moderation', 'notify_edit_pst_moderation', 'notify_post_proposed'
     * @param integer User ID who executed the action which will be notified, or NULL if it was executed by current logged in User
     * @param string|false Additional check with collection subscription: 'sub_items', 'sub_items_mod', 'sub_comments'
     * @return array Key - User ID, Value - perm level
     */
   
function get_moderators( $notify_setting_name, $executed_by_userid = NULL, $check_coll_subscription = false )
    {
        global
$Settings, $DB;

        if(
$executed_by_userid === NULL && is_logged_in() )
        {    
// Use current user by default:
           
global $current_User;
           
$executed_by_userid = $current_User->ID;
        }

       
$notify_condition = 'uset_value IS NOT NULL AND uset_value <> "0"';
        if(
$Settings->get( 'def_'.$notify_setting_name ) )
        {
           
$notify_condition = '( uset_value IS NULL OR ( '.$notify_condition.' ) )';
        }
        if(
$check_coll_subscription !== false )
        {    
// Notify moderators which selected to be notified per collection (if it is enabled by collection setting):
           
$notify_condition = '( '.$check_coll_subscription.' = 1 OR ( '.$notify_condition.' ) )';
        }

       
// Select user_ids with the corresponding item edit permission on this item's blog
       
$SQL = new SQL( 'Get moderators "'.$notify_setting_name.'" for Item #'.$this->ID );
       
$SQL->SELECT( 'user_ID, IF( grp_perm_blogs = "editall" OR user_ID = blog_owner_user_ID, "all", IF( IFNULL( bloguser_perm_edit + 0, 0 ) > IFNULL( bloggroup_perm_edit + 0, 0 ), bloguser_perm_edit, bloggroup_perm_edit ) ) as perm' );
       
$SQL->FROM( 'T_users' );
       
$SQL->FROM_add( 'LEFT JOIN T_blogs ON ( blog_ID = '.$this->get_blog_ID().' )' );
       
$SQL->FROM_add( 'LEFT JOIN T_coll_user_perms ON (blog_advanced_perms <> 0 AND user_ID = bloguser_user_ID AND bloguser_blog_ID = '.$this->get_blog_ID().' )' );
       
$SQL->FROM_add( 'LEFT JOIN T_coll_group_perms ON (blog_advanced_perms <> 0 AND user_grp_ID = bloggroup_group_ID AND bloggroup_blog_ID = '.$this->get_blog_ID().' )' );
       
$SQL->FROM_add( 'LEFT JOIN T_users__usersettings ON uset_user_ID = user_ID AND uset_name = "'.$notify_setting_name.'"' );
       
$SQL->FROM_add( 'LEFT JOIN T_groups ON grp_ID = user_grp_ID' );
       
$SQL->FROM_add( 'LEFT JOIN T_subscriptions ON sub_coll_ID = blog_ID AND sub_user_ID = user_ID' );
       
$SQL->WHERE( $notify_condition );
       
$SQL->WHERE_and( 'user_status IN ( "activated", "autoactivated", "manualactivated" )' );
       
$SQL->WHERE_and( '( bloguser_perm_edit IS NOT NULL AND bloguser_perm_edit <> "no" AND bloguser_perm_edit <> "own" )
                OR ( bloggroup_perm_edit IS NOT NULL AND bloggroup_perm_edit <> "no" AND bloggroup_perm_edit <> "own" )
                OR ( grp_perm_blogs = "editall" ) OR ( user_ID = blog_owner_user_ID )'
);
        if(
$executed_by_userid !== NULL )
        {    
// Don't notify the user who just created/updated this post:
           
$SQL->WHERE_and( 'user_ID != '.$DB->quote( $executed_by_userid ) );
        }

        return
$DB->get_assoc( $SQL );
    }


   
/**
     * Send "post may need moderation" notifications for those users who have permission to moderate this post and would like to receive these notifications.
     *
     * @param integer User ID who executed the action which will be notified, or NULL if it was executed by current logged in User
     * @param boolean TRUE if it is notification about new item, FALSE - for edited item
     * @return array the notified user ids
     */
   
function send_moderation_emails( $executed_by_userid = NULL, $is_new_item = false )
    {
        global
$Messages;

       
$notify_moderation_setting_name = ( $is_new_item ? 'notify_post_moderation' : 'notify_edit_pst_moderation' );
       
// Notify moderators which selected to be notified per collection (if it is enabled by collection setting):
       
$check_coll_subscription = ( ! $is_new_item && $this->get_Blog()->get_setting( 'allow_item_mod_subscriptions' ) ? 'sub_items_mod' : false );

       
// Get all users who are post moderators in this Item's blog:
       
$post_moderators = $this->get_moderators( $notify_moderation_setting_name, $executed_by_userid, $check_coll_subscription );

       
$post_creator_User = & $this->get_creator_User();
        if( isset(
$post_moderators[$post_creator_User->ID] ) )
        {    
// Don't notify the user who just created this Item:
           
unset( $post_moderators[$post_creator_User->ID] );
        }

       
// Collect all notified User IDs in this array:
       
$notified_user_IDs = array();

        if( empty(
$post_moderators ) )
        {
// There are no moderator users who would like to receive notificaitons
           
return $notified_user_IDs;
        }

       
$post_creator_level = $post_creator_User->level;
       
$UserCache = & get_UserCache();
       
$UserCache->load_list( array_keys( $post_moderators ) );

        foreach(
$post_moderators as $moderator_ID => $perm )
        {
           
$moderator_User = $UserCache->get_by_ID( $moderator_ID );
            if( (
$perm == 'lt' ) && ( $moderator_User->level <= $post_creator_level ) )
            {
// User has no permission moderate this post
               
continue;
            }
            if( (
$perm == 'le' ) && ( $moderator_User->level < $post_creator_level ) )
            {
// User has no permission moderate this post
               
continue;
            }

           
$moderator_user_Group = $moderator_User->get_Group();
           
$notify_full = $moderator_user_Group->check_perm( 'post_moderation_notif', 'full' );

           
$email_template_params = array(
               
'locale'         => $moderator_User->locale,
               
'notify_full'    => $notify_full,
               
'Item'           => $this,
               
'recipient_User' => $moderator_User,
               
'notify_type'    => 'moderator',
               
'is_new_item'    => $is_new_item,
            );

           
locale_temp_switch( $moderator_User->locale );

            if(
$this->status == 'draft' || $this->status == 'review' )
            {
               
/* TRANS: Subject of the mail to send on new posts to moderators. First %s is blog name, the second %s is the item's title. */
               
$subject = T_('[%s] New post awaiting moderation: "%s"');
            }
            else
            {
               
/* TRANS: Subject of the mail to send on new posts to moderators. First %s is blog name, the second %s is the item's title. */
               
$subject = T_('[%s] New post may need moderation: "%s"');
            }
           
$subject = sprintf( $subject, $this->Blog->get('shortname'), $this->get('title') );

           
// Send the email:
           
if( send_mail_to_User( $moderator_ID, $subject, 'post_new', $email_template_params ) )
            {    
// A send notification email request to the user with $moderator_ID ID was processed:
               
$notified_user_IDs[] = $moderator_ID;
            }

           
locale_restore_previous();
        }

       
// Record that we have notified the moderators (for info only):
       
$this->set( 'notifications_flags', 'moderators_notified' );
       
// Save the new processing status to DB, but do not update last edited by user, slug or child custom fields:
       
$this->dbupdate( false, false, false );

       
$this->display_notification_message( sprintf( T_('Sending %d email notifications to moderators.'), count( $notified_user_IDs ) ) );

        return
$notified_user_IDs;
    }


   
/**
     * Send "post proposed change" notifications for those users who have permission to moderate this post and would like to receive these notifications.
     *
     * @param integer Version ID of new proposed change
     */
   
function send_proposed_change_notification( $iver_ID )
    {
        global
$Messages, $current_User;

        if( !
is_logged_in() )
        {    
// Only logged in user can propose a change
           
return;
        }

       
// Get all users who are post moderators in this Item's blog:
       
$post_moderators = $this->get_moderators( 'notify_post_proposed' );

        if( empty(
$post_moderators ) )
        {
// There are no moderator users who would like to receive notificaitons
           
return;
        }

       
// Clear revision in order to use current data in the email message:
       
$this->clear_revision();

       
// Collect all notified User IDs in this array:
       
$notified_users_num = 0;

       
$post_creator_User = & $this->get_creator_User();
       
$post_creator_level = $post_creator_User->level;
       
$UserCache = & get_UserCache();
       
$UserCache->load_list( array_keys( $post_moderators ) );

        foreach(
$post_moderators as $moderator_ID => $perm )
        {
           
$moderator_User = $UserCache->get_by_ID( $moderator_ID );
            if( (
$perm == 'lt' ) && ( $moderator_User->level <= $post_creator_level ) )
            {
// User has no permission moderate this post
               
continue;
            }
            if( (
$perm == 'le' ) && ( $moderator_User->level < $post_creator_level ) )
            {
// User has no permission moderate this post
               
continue;
            }

           
$moderator_user_Group = $moderator_User->get_Group();

           
$email_template_params = array(
               
'iver_ID'        => $iver_ID,
               
'Item'           => $this,
               
'recipient_User' => $moderator_User,
               
'proposer_User'  => $current_User,
            );

           
locale_temp_switch( $moderator_User->locale );

           
// TRANS: Subject of the mail to send on a post proposed change to moderators. First %s is blog name, the second %s is the item's title.
           
$subject = sprintf( T_('[%s] New change was proposed on: "%s"'), $this->get_Blog()->get( 'shortname' ), $this->get( 'title' ) );

           
// Send the email:
           
if( send_mail_to_User( $moderator_ID, $subject, 'post_proposed_change', $email_template_params ) )
            {    
// A send notification email request to the user with $moderator_ID ID was processed:
               
$notified_users_num++;
            }

           
locale_restore_previous();
        }

       
$this->display_notification_message( sprintf( T_('Sending %d email notifications to moderators.'), $notified_users_num ) );
    }


   
/**
     * Send "post assignment" notifications for user who have been assigned to this post and would like to receive these notifications.
     *
     * @param integer User ID who executed the action which will be notified, or NULL if it was executed by current logged in User
     * @return array the notified user ids
     */
   
function send_assignment_notification( $executed_by_userid = NULL )
    {
        global
$Messages, $UserSettings;

       
$notified_user_IDs = array();

        if(
$executed_by_userid === NULL && is_logged_in() )
        {    
// Use current user by default:
           
global $current_User;
           
$executed_by_userid = $current_User->ID;
        }

        if(
$this->assigned_user_ID && $executed_by_userid != $this->assigned_user_ID )
        {
// Item has assigned user and the assigned user is not the one who created/updated this post:
           
$UserCache = & get_UserCache();
           
$principal_User = $UserCache->get_by_ID( $executed_by_userid, false, false );
           
$assigned_User = $this->get_assigned_User();

            if(
$assigned_User &&
                   
$UserSettings->get( 'notify_post_assignment', $assigned_User->ID ) &&
                   
$assigned_User->check_perm( 'blog_can_be_assignee', 'view', false, $this->get_blog_ID() ) )
            {    
// Assigned user wants to receive post assignment notifications and can be assigned to items of this Item's collection:
               
$user_Group = $assigned_User->get_Group();
               
$notify_full = $user_Group->check_perm( 'post_assignment_notif', 'full' );

               
$email_template_params = array(
                   
'locale'         => $assigned_User->locale,
                   
'notify_full'    => $notify_full,
                   
'Item'           => $this,
                   
'principal_User' => $principal_User,
                   
'recipient_User' => $assigned_User,
                );

               
locale_temp_switch( $assigned_User->locale );

               
/* TRANS: Subject of the mail to send on assignment of posts to a user. First %s is blog name, the second %s is the item's title. */
               
$subject = T_('[%s] Post assignment: "%s"');
               
$subject = sprintf( $subject, $this->Blog->get('shortname'), $this->get('title') );

               
// Send the email:
               
if( send_mail_to_User( $assigned_User->ID, $subject, 'post_assignment', $email_template_params ) )
                {    
// A send notification email request to the assigned user was processed:
                   
$notified_user_IDs[] = $assigned_User->ID;
                   
$this->display_notification_message( T_('Sending email notification to assigned user.') );
                }

               
locale_restore_previous();
            }
        }

        return
$notified_user_IDs;
    }


   
/**
     * Send email notifications to subscribed users
     *
     * @todo fp>> shall we notify suscribers of blog were this is in extra-cat? blueyed>> IMHO yes.
     *
     * @param integer User ID who executed the action which will be notified, or NULL if it was executed by current logged in User
     * @param boolean TRUE if it is notification about new item, FALSE - for edited item
     * @param array Already notified user ids, or NULL if it is not the case
     * @param boolean|string Force sending notifications for members:
     *                       false - Auto mode depending on current item statuses
     *                       'skip' - Skip notifications
     *                       'force' - Force notifications
     * @param boolean|string Force sending notifications for community (use same values of third param)
     * @param boolean|string 'cron_job' - to log messages for cron job, FALSE - to don't log
     * @return array Notified flags: 'members_notified', 'community_notified'
     */
   
function send_email_notifications( $executed_by_userid = NULL, $is_new_item = false, $already_notified_user_IDs = NULL, $force_members = false, $force_community = false, $log_messages = false )
    {
        global
$DB, $debug, $Messages, $Debuglog;

        if(
$executed_by_userid === NULL && is_logged_in() )
        {    
// Use current user by default:
           
global $current_User;
           
$executed_by_userid = $current_User->ID;
        }

       
$edited_Blog = & $this->get_Blog();

        if( !
$edited_Blog->get_setting( 'allow_subscriptions' ) )
        {    
// Subscriptions not enabled!
           
$this->display_notification_message( T_('Skipping email notifications to subscribers because subscriptions are turned Off for this collection.'), $log_messages );
            return array();
        }

        if( !
$this->notifications_allowed() )
        {    
// Don't send notifications about some post/usages like "special":
            // Note: this is a safety but this case should never happen, so don't make translators work on this:
           
$this->display_notification_message( 'This post type/usage cannot support notifications: skipping notifications...', $log_messages );
            return array();
        }

        if( !
in_array( $this->get( 'status' ), array( 'protected', 'community', 'published' ) ) )
        {    
// Don't send notifications about items with not allowed status:
           
$status_titles = get_visibility_statuses( '', array() );
           
$status_title = isset( $status_titles[ $this->get( 'status' ) ] ) ? $status_titles[ $this->get( 'status' ) ] : $this->get( 'status' );
           
$this->display_notification_message( sprintf( T_('Skipping email notifications to subscribers because status is still: %s.'), $status_title ), $log_messages );
            return array();
        }

        if(
$force_members == 'skip' && $force_community == 'skip' )
        {    
// Skip subscriber notifications because of it is forced by param:
           
$this->display_notification_message( T_('Skipping email notifications to subscribers.'), $log_messages );
            return array();
        }

        if(
$force_members == 'force' && $force_community == 'force' )
        {    
// Force to members and community:
           
$this->display_notification_message( T_('Force sending email notifications to subscribers...'), $log_messages );
        }
        elseif(
$force_members == 'force' )
        {    
// Force to members only:
           
$this->display_notification_message( T_('Force sending email notifications to subscribed members...'), $log_messages );
        }
        elseif(
$force_community == 'force' )
        {    
// Force to community only:
           
$this->display_notification_message( T_('Force sending email notifications to other subscribers...'), $log_messages );
        }
        else
        {    
// Check if email notifications can be sent for this item currently:

            // Some post usages should not trigger notifications to subscribers (moderators are notified earlier in the process, so they will be notified)
            // fp> I think the only usage that makes sense to send automatic notifications to subscribers is "Post"
           
if( $this->get_type_setting( 'usage' ) != 'post' )
            {    
// Don't send outbound pings for items that are not regular posts:
               
$this->display_notification_message( T_('This post type/usage doesn\'t need notifications by default: skipping notifications...'), $log_messages );
                return array();
            }
        }

       
$notify_members = false;
       
$notify_community = false;

        if(
$this->get( 'status' ) == 'protected' )
        {    
// If the post is visible for members only...
           
if( $force_members == 'force' || ( ! $this->check_notifications_flags( 'members_notified' ) && $this->get_type_setting( 'usage' ) == 'post' ) )
            {    
// Members have not been notified yet OR Force sending, do so:
               
$notify_members = true;
            }
        }
        elseif(
$this->get( 'status' ) == 'community' || $this->get( 'status' ) == 'published' )
        {    
// If the post is visible to the community or is public...
           
if( $force_members == 'force' || ( ! $this->check_notifications_flags( 'members_notified' ) && $this->get_type_setting( 'usage' ) == 'post' ) )
            {    
// Members have not been notified yet OR Force sending, do so:
               
$notify_members = true;
            }
            if(
$force_community == 'force' || ( ! $this->check_notifications_flags( 'community_notified' ) && $this->get_type_setting( 'usage' ) == 'post' ) )
            {    
// Community have not been notified yet OR Force sending, do so:
               
$notify_community = true;
            }
        }

        if( !
$notify_members && ! $notify_community )
        {    
// Everyone has already been notified, nothing to do:
           
$this->display_notification_message( T_('Skipping email notifications to subscribers because they were already notified.'), $log_messages );
            return array();
        }

        if(
$notify_members && $force_members == 'skip' )
        {    
// Skip email notifications to members because it is forced by param:
           
$this->display_notification_message( T_('Skipping email notifications to subscribed members.'), $log_messages );
           
$notify_members = false;
        }
        if(
$notify_community && $force_community == 'skip' )
        {    
// Skip email notifications to community because it is forced by param:
           
$this->display_notification_message( T_('Skipping email notifications to other subscribers.'), $log_messages );
           
$notify_community = false;
        }

       
// Set flags what really users will be notified below:
       
$notified_flags = array();
        if(
$notify_members )
        {    
// If members should be notified:
           
$notified_flags[] = 'members_notified';
        }
        if(
$notify_community )
        {    
// If community should be notified:
           
$notified_flags[] = 'community_notified';
        }

        if( !
$notify_members && ! $notify_community )
        {    
// All notifications are skipped by requested params:
           
return $notified_flags;
        }

       
$Debuglog->add( 'Ready to send notifications to members? : '.($notify_members ? 'Yes' : 'No' ), 'notifications' );
       
$Debuglog->add( 'Ready to send notifications to community? : '.($notify_community ? 'Yes' : 'No' ), 'notifications' );

       
$notify_users = array();

       
// Get list of users who want to be notified when his login is mentioned in the item content by @user's_login:
       
$mentioned_user_IDs = get_mentioned_user_IDs( 'item', $this->get( 'content' ), $already_notified_user_IDs );
        foreach(
$mentioned_user_IDs as $mentioned_user_ID )
        {
           
$notify_users[ $mentioned_user_ID ] = 'post_mentioned';
        }

       
// Get list of users who want to be notified:
        // TODO: also use extra cats/blogs??
       
$sql = 'SELECT user_ID
                FROM (
                    SELECT DISTINCT sub_user_ID AS user_ID
                    FROM T_subscriptions
                    INNER JOIN T_users ON user_ID = sub_user_ID
                    WHERE sub_coll_ID = '
.$this->get_blog_ID().'
                    AND sub_items <> 0
                    AND user_status IN ( "activated", "autoactivated", "manualactivated" )

                    UNION

                    SELECT user_ID
                    FROM T_coll_settings AS opt
                    INNER JOIN T_blogs ON ( blog_ID = opt.cset_coll_ID AND blog_advanced_perms = 1 )
                    INNER JOIN T_coll_settings AS sub ON ( sub.cset_coll_ID = opt.cset_coll_ID AND sub.cset_name = "allow_subscriptions" AND sub.cset_value = 1 )
                    LEFT JOIN T_coll_group_perms ON ( bloggroup_blog_ID = opt.cset_coll_ID AND bloggroup_ismember = 1 )
                    LEFT JOIN T_users ON ( user_grp_ID = bloggroup_group_ID )
                    LEFT JOIN T_subscriptions ON ( sub_coll_ID = opt.cset_coll_ID AND sub_user_ID = user_ID )
                    WHERE opt.cset_coll_ID = '
.$this->get_blog_ID().'
                        AND opt.cset_name = "opt_out_subscription"
                        AND opt.cset_value = 1
                        AND NOT user_ID IS NULL
                        AND ( ( sub_items IS NULL OR sub_items = 1 ) )
                        AND user_status IN ( "activated", "autoactivated", "manualactivated" )

                    UNION

                    SELECT sug_user_ID
                    FROM T_coll_settings AS opt
                    INNER JOIN T_blogs ON ( blog_ID = opt.cset_coll_ID AND blog_advanced_perms = 1 )
                    INNER JOIN T_coll_settings AS sub ON ( sub.cset_coll_ID = opt.cset_coll_ID AND sub.cset_name = "allow_subscriptions" AND sub.cset_value = 1 )
                    LEFT JOIN T_coll_group_perms ON ( bloggroup_blog_ID = opt.cset_coll_ID AND bloggroup_ismember = 1 )
                    LEFT JOIN T_users__secondary_user_groups ON ( sug_grp_ID = bloggroup_group_ID )
                    LEFT JOIN T_subscriptions ON ( sub_coll_ID = opt.cset_coll_ID AND sub_user_ID = sug_user_ID )
                    LEFT JOIN T_users ON ( user_ID = sug_user_ID )
                    WHERE opt.cset_coll_ID = '
.$this->get_blog_ID().'
                        AND opt.cset_name = "opt_out_subscription"
                        AND opt.cset_value = 1
                        AND NOT sug_user_ID IS NULL
                        AND ( ( sub_items IS NULL OR sub_items = 1 ) )
                        AND user_status IN ( "activated", "autoactivated", "manualactivated" )

                    UNION

                    SELECT bloguser_user_ID
                    FROM T_coll_settings AS opt
                    INNER JOIN T_blogs ON ( blog_ID = opt.cset_coll_ID AND blog_advanced_perms = 1 )
                    INNER JOIN T_coll_settings AS sub ON ( sub.cset_coll_ID = opt.cset_coll_ID AND sub.cset_name = "allow_subscriptions" AND sub.cset_value = 1 )
                    LEFT JOIN T_coll_user_perms ON ( bloguser_blog_ID = opt.cset_coll_ID AND bloguser_ismember = 1 )
                    LEFT JOIN T_subscriptions ON ( sub_coll_ID = opt.cset_coll_ID AND sub_user_ID = bloguser_user_ID )
                    LEFT JOIN T_users ON ( user_ID = sub_user_ID )
                    WHERE opt.cset_coll_ID = '
.$this->get_blog_ID().'
                        AND opt.cset_name = "opt_out_subscription"
                        AND opt.cset_value = 1
                        AND NOT bloguser_user_ID IS NULL
                        AND ( ( sub_items IS NULL OR sub_items = 1 ) )
                        AND user_status IN ( "activated", "autoactivated", "manualactivated" )
                ) AS users
                WHERE NOT user_ID IS NULL'
;

        if( ! empty(
$already_notified_user_IDs ) )
        {
           
$sql .= ' AND user_ID NOT IN ( '.implode( ',', $already_notified_user_IDs ).' )';
        }
        if(
$executed_by_userid !== NULL )
        {
           
$sql .= ' AND user_ID != '.$DB->quote( $executed_by_userid );
        }

       
$notify_list = $DB->get_col( $sql, 0, 'Get list of users who want to be notified (and have not yet been notified) about new items on collection #'.$this->get_blog_ID() );

       
// Preprocess list:
       
foreach( $notify_list as $notify_user_ID )
        {
            if( ! isset(
$notify_users[ $notify_user_ID ] ) )
            {    
// Don't rewrite a notify type if user already is notified by other type before:
               
$notify_users[ $notify_user_ID ] = 'subscription';
            }
        }

       
$notify_user_IDs = array_keys( $notify_users );

       
$Debuglog->add( 'Number of users who want to be notified (and have not yet been notified) about new items on collection #'.$this->get_blog_ID().' = '.count( $notify_users ), 'notifications' );
       
$Debuglog->add( 'First 10 user IDs: '.implode( ',', array_slice( $notify_user_IDs, 0, 10 ) ), 'notifications' );

       
// Load all users who will be notified:
       
$UserCache = & get_UserCache();
       
$UserCache->load_list( $notify_user_IDs );

       
$members_count = 0;
       
$community_count = 0;
        foreach(
$notify_users as $user_ID => $notify_type )
        {    
// Check for each subscribed User, if we can send a notification to him depending on current request and Item settings:

           
if( ! ( $notify_User = & $UserCache->get_by_ID( $user_ID, false, false ) ) )
            {    
// Invalid User, Skip it:
               
$Debuglog->add( 'User #'.$user_ID.' is invalid.', 'notifications'  );
                unset(
$notify_users[ $user_ID ] );
                continue;
            }

           
// Check if the User is member of the collection:
           
$is_member = $notify_User->check_perm( 'blog_ismember', 'view', false, $this->get_blog_ID() );

            if(
$notify_members && $notify_community )
            {    
// We can notify all subscribed users:
               
if( $is_member )
                {    
// Count subscribed member:
                   
$members_count++;
                }
                else
                {    
// Count other subscriber:
                   
$community_count++;
                }
            }
            elseif(
$notify_members )
            {    
// We should notify only members:
               
if( $is_member )
                {    
// Count subscribed member:
                   
$members_count++;
                }
                else
                {    
// Skip not member:
                   
$Debuglog->add( 'User #'.$user_ID.' is a not a member but at this time, we only want to notify members.', 'notifications'  );
                    unset(
$notify_users[ $user_ID ] );
                }
            }
            else
            {    
// We should notify only community users:
               
if( ! $is_member )
                {    
// Count subscribed community user:
                   
$community_count++;
                }
                else
                {    
// Skip member:
                   
$Debuglog->add( 'User #'.$user_ID.' is a member but we at this time, we only want to notify community.', 'notifications'  );
                    unset(
$notify_users[ $user_ID ] );
                }
            }
        }

       
$Debuglog->add( 'Number of users who are allowed to be notified about new items on collection #'.$this->get_blog_ID().' = '.count( $notify_users ), 'notifications' );
       
$Debuglog->add( 'First 10 user IDs: '.implode( ',', array_slice( $notify_user_IDs, 0, 10 ) ), 'notifications' );

        if(
$notify_members )
        {    
// Display a message to know how many members are notified:
           
$this->display_notification_message( sprintf( T_('Sending %d email notifications to subscribed members.'), $members_count ), $log_messages );
        }
        if(
$notify_community )
        {    
// Display a message to know how many community users are notified:
           
$this->display_notification_message( sprintf( T_('Sending %d email notifications to other subscribers.'), $community_count ), $log_messages );
        }

        if( empty(
$notify_users ) )
        {    
// No-one to notify:
           
return $notified_flags;
        }

       
/*
         * We have a list of User IDs to notify:
         */
       
$this->get_creator_User();

       
// Load a list with the blocked emails in cache:
       
load_blocked_emails( $notify_user_IDs );

       
// Send emails:
       
$notified_users_num = 0;
       
$cache_by_locale = array();
        foreach(
$notify_users as $user_ID => $notify_type )
        {
           
$notify_User = & $UserCache->get_by_ID( $user_ID, false, false );
            if( empty(
$notify_User ) )
            {    
// skip invalid users:
               
continue;
            }

           
$notify_email = $notify_User->get( 'email' );
            if( empty(
$notify_email ) )
            {    
// skip users with empty email address:
               
continue;
            }
           
$notify_locale = $notify_User->get( 'locale' );
           
$notify_user_Group = $notify_User->get_Group();

           
$notify_full = $notify_user_Group->check_perm( 'post_subscription_notif', 'full' );
            if( ! isset(
$cache_by_locale[$notify_locale]) )
            {    
// No message for this locale generated yet:
               
locale_temp_switch( $notify_locale );

               
/* TRANS: Subject of the mail to send on new posts to subscribed users. First %s is blog name, the second %s is the item's title. */
               
$cache_by_locale[$notify_locale]['subject'] = sprintf( T_('[%s] New post: "%s"'), $edited_Blog->get('shortname'), $this->get('title') );

               
locale_restore_previous();
            }

           
$email_template_params = array(
                   
'locale'         => $notify_locale,
                   
'notify_full'    => $notify_full,
                   
'Item'           => $this,
                   
'recipient_User' => $notify_User,
                   
'notify_type'    => $notify_type,
                   
'is_new_item'    => $is_new_item,
                );

            if(
$debug >= 2 )
            {
               
$message_content = mail_template( 'post_new', 'txt', $email_template_params );
                echo
"<p>Sending notification to $notify_email:<pre>$message_content</pre>";
            }

            if(
send_mail_to_User( $user_ID, $cache_by_locale[$notify_locale]['subject'], 'post_new', $email_template_params ) )
            {
               
$notified_users_num++;
                if(
$log_messages == 'cron_job' )
                {    
// Log success mail sending for cron job:
                   
cron_log_action_end( 'User '.$notify_User->get_identity_link().' has been notified' );
                }
            }
            elseif(
$log_messages == 'cron_job' )
            {    
// Log failed mail sending for cron job:
               
global $mail_log_message;
               
cron_log_action_end( 'User '.$notify_User->get_identity_link().' could not be notified because of error: '
                   
.'"'.( empty( $mail_log_message ) ? 'Unknown Error' : $mail_log_message ).'"', 'warning' );
            }

           
blocked_emails_memorize( $notify_User->email );
        }

       
blocked_emails_display( $log_messages );

        if(
$log_messages == 'cron_job' )
        {    
// Log how much users were really notified:
           
cron_log_append( sprintf( '%d of %d users have been notified!', $notified_users_num, count( $notify_users ) ) );
        }

        return
$notified_flags;
    }


   
/**
     * Send outbound pings for a post
     *
     * @param boolean|string Force sending outbound pings:
     *                       false - Auto mode depending on current item statuses
     *                       'skip' - Skip notifications
     *                       'force' - Force notifications
     * @param boolean|string 'cron_job' - to log messages for cron job, FALSE - to don't log
     * @return boolean TRUE on success
     */
   
function send_outbound_pings( $force_pings = false, $log_messages = false )
    {
        global
$Plugins, $baseurl, $Messages, $evonetsrv_host, $allow_post_pings_on_localhost;

        if( !
$this->notifications_allowed() )
        {    
// Don't send pings about some post/usages like "special":
            // Note: this is a safety but this case should never happen, so don't make translators work on this:
           
$this->display_notification_message( 'This post type/usage cannot support pings: skipping pings...', $log_messages );
            return
false;
        }

        if(
$this->get( 'status' ) != 'published' )
        {    
// Don't send pings if item is not 'public':
           
$this->display_notification_message( T_('Skipping outbound pings because item is not published yet.'), $log_messages );
            return
false;
        }

        if(
$force_pings == 'skip' )
        {    
// Skip pings because it is forced by param:
           
$this->display_notification_message( T_('Skipping outbound pings.'), $log_messages );
            return
false;
        }

        if(
$force_pings == 'force' )
        {    
// Force pings:
           
$this->display_notification_message( T_('Force sending outbound pings...'), $log_messages );
        }
        else
        {    
// Check if pings can be sent for this item currently:

           
if( $this->check_notifications_flags( 'pings_sent' ) )
            {    
// Don't send pings if they have already been sent:
               
$this->display_notification_message( T_('Skipping outbound pings because they were already sent.'), $log_messages );
                return
false;
            }

           
// Some post usages should not trigger notifications to subscribers (moderators are notified earlier in the process, so they will be notified)
            // fp> I think the only usage that makes sense to send automatic notifications to subscribers is "Post"
           
if( $this->get_type_setting( 'usage' ) != 'post' )
            {    
// Don't send outbound pings for items that are not regular posts:
               
$this->display_notification_message( T_('This post type/usage doesn\'t need pings by default: skipping pings...'), $log_messages );
                return
false;
            }
        }

       
// init result
       
$r = true;

        if( empty(
$allow_post_pings_on_localhost ) &&
           
$evonetsrv_host != 'localhost' && // OK if we are pinging locally anyway ;)
           
( preg_match( '#^http://localhost[/:]#', $baseurl ) ||
             
preg_match( '~^\w+://[^/]+\.local/~', $baseurl ) ) ) /* domain ending in ".local" */
       
{    // Don't send pings from localhost:
           
$this->display_notification_message( T_('Skipping pings (Running on localhost).'), $log_messages );
            return
false;
        }
        else
        {    
// Send pings:
           
$this->display_notification_message( T_('Trying to find plugins for sending outbound pings...'), $log_messages );

           
load_funcs('xmlrpc/model/_xmlrpc.funcs.php');

           
$this->load_Blog();
           
$ping_plugins = trim( $this->Blog->get_setting( 'ping_plugins' ) );
           
$ping_plugins = empty( $ping_plugins ) ? array() :  array_unique( explode( ',', $this->Blog->get_setting( 'ping_plugins' ) ) );

            foreach(
$ping_plugins as $plugin_code )
            {
               
$Plugin = & $Plugins->get_by_code( $plugin_code );

                if(
$Plugin )
                {
                   
$ping_messages = array();
                   
$ping_messages[] = array(
                       
'message' => isset( $Plugin->ping_service_process_message ) ? $Plugin->ping_service_process_message : sprintf( /* TRANS: %s is a ping service name */ T_('Pinging %s...'), $Plugin->ping_service_name ),
                       
'type' => 'note' );
                   
$params = array( 'Item' => & $this, 'xmlrpcresp' => NULL, 'display' => false );

                   
$r = $Plugin->ItemSendPing( $params ) && $r;

                    if( ! empty(
$params['xmlrpcresp'] ) )
                    {
                        if(
$params['xmlrpcresp'] instanceof xmlrpcresp )
                        {
                           
// dh> TODO: let xmlrpc_displayresult() handle $Messages (e.g. "error", but should be connected/after the "Pinging %s..." from above)
                           
ob_start();
                           
xmlrpc_displayresult( $params['xmlrpcresp'], true );
                           
$ping_messages[] = array(
                               
'message' => ob_get_contents(),
                               
'type' => 'note' );
                           
ob_end_clean();
                        }
                        elseif(
is_array( $params['xmlrpcresp'] ) )
                        {
                           
$ping_messages = array_merge( $ping_messages, $params['xmlrpcresp'] );
                        }
                        else
                        {
                           
$ping_messages[] = $params['xmlrpcresp'];
                        }
                    }

                   
$current_type = NULL;
                   
$current_title = NULL;
                   
$current_message = NULL;

                    foreach(
$ping_messages as $message )
                    {
                        if(
is_array( $message ) )
                        {
                           
$loop_type = empty( $message['type'] ) ? 'note' : $message['type'];
                           
$loop_title = empty( $message['title'] ) ? T_('Sending notifications:') : $message['title'];
                           
$loop_message = $message['message'];
                        }
                        else
                        {
                           
$loop_type = 'note';
                           
$loop_title = T_('Sending notifications:');
                           
$loop_message = $message;
                        }

                        if( empty(
$current_type ) ) $current_type = $loop_type;
                        if( empty(
$current_title ) ) $current_title = $loop_title;

                        if(
$loop_type == $current_type && $loop_title == $current_title )
                        {
                            if( empty(
$current_message ) )
                            {
                               
$current_message = $loop_message;
                            }
                            else
                            {
                               
$current_message .= '<br>'.$loop_message;
                            }
                        }
                        else
                        {
                            if(
$log_messages == 'cron_job' )
                            {
                               
cron_log_append( $current_message."\n", $current_type );
                            }
                            else
                            {
                               
$Messages->add_to_group( $current_message, $current_type, $current_title );
                            }
                           
$current_message = $loop_message;
                           
$current_type = $loop_type;
                           
$current_title = $loop_title;
                        }
                    }

                    if( ! empty(
$current_message ) )
                    {    
// Display last message:
                       
$this->display_notification_message( $current_message, $log_messages, $current_type, $current_title );
                    }
                }
            }
        }

       
// Record that we have just pinged:
       
$this->set( 'notifications_flags', 'pings_sent' );

        return
$r;
    }


   
/**
     * Callback user for footer()
     */
   
function replace_callback( $matches )
    {
        switch(
$matches[1] )
        {
            case
'perm_url':
            case
'item_perm_url':
                return
$this->get_permanent_url();

            case
'title':
            case
'item_title':
                return
$this->get( 'title' );

            case
'excerpt':
                return
$this->get_excerpt();

            case
'author':
                return
$this->get('t_author');

            case
'author_login':
                return
$this->get_creator_login();

            default:
                return
$matches[1];
        }
    }

   
/**
     * Get a member param by its name
     *
     * @param mixed Name of parameter
     * @return mixed Value of parameter
     */
   
function get( $parname )
    {
        switch(
$parname )
        {
            case
't_author':
               
// Text: author
               
$this->get_creator_User();
                return
$this->creator_User->get( 'preferredname' );

            case
't_assigned_to':
               
// Text: assignee
               
if( ! $this->get_assigned_User() )
                {
                    return
'';
                }
                return
$this->assigned_User->get( 'preferredname' );

            case
't_status':
               
// Text status:
               
$post_statuses = get_visibility_statuses();
                return
$post_statuses[ $this->get_from_revision( 'status' ) ];

            case
't_extra_status':
               
$ItemStatusCache = & get_ItemStatusCache();
                if( ! (
$Element = & $ItemStatusCache->get_by_ID( $this->pst_ID, true, false ) ) )
                {
// No status:
                   
return T_('No status');
                }
                return
$Element->get_name();

            case
't_type':
               
// Post type (name):
               
if( empty($this->ityp_ID) )
                {
                    return
'';
                }

               
$ItemTypeCache = & get_ItemTypeCache();
               
$type_Element = & $ItemTypeCache->get_by_ID( $this->ityp_ID );
                return
$type_Element->get_name();

            case
't_priority':
                return
$this->priorities[ $this->priority ];

            case
'pingsdone':
               
// Have pings been sent? (should only happen once the post has visibility 'public')
                // return ($this->post_notifications_status == 'finished'); // Deprecated by fp 2006-08-21 -- TODO: this should now become an alias of "pings_sent"
               
return $this->check_notifications_flags( 'pings_sent' );

            case
'excerpt':
                return
$this->get_excerpt();

            case
'notifications_flags':
                return empty(
$this->notifications_flags ) ? array() : explode( ',', $this->notifications_flags );

            case
'order':
               
// Get item order in main category:
               
return $this->get_order();

            case
'title':
            case
'content':
            case
'status':
                return
$this->get_from_revision( $parname );
        }

        return
parent::get( $parname );
    }


   
/**
     * Load item orders per categories
     */
   
function load_orders()
    {
        if( ! isset(
$this->orders ) && ( $this->ID > 0 || isset( $this->parent_item_ID ) ) )
        {    
// Initialize item orders in all assigned categories:
           
$item_ID = ( $this->ID > 0 ) ? $this->ID : $this->parent_item_ID;
            global
$DB;
           
$SQL = new SQL( 'Get all orders per categories of Item #'.$item_ID );
           
$SQL->SELECT( 'cat_ID, cat_blog_ID, postcat_order' );
           
$SQL->FROM( 'T_postcats' );
           
$SQL->FROM_add( 'INNER JOIN T_categories ON cat_ID = postcat_cat_ID' );
           
$SQL->WHERE( 'postcat_post_ID = '.$item_ID );
           
$orders = $DB->get_results( $SQL );
           
$this->orders = array();
           
$this->orders_per_coll = array();
            foreach(
$orders as $order )
            {
               
$this->orders[ $order->cat_ID ] = $order->postcat_order;
               
// Initialize categories per collection, useful in cross-posted mode:
               
if( ! isset( $this->orders_per_coll[ $order->cat_blog_ID ] ) )
                {
                   
$this->orders_per_coll[ $order->cat_blog_ID ] = array();
                }
               
$this->orders_per_coll[ $order->cat_blog_ID ][ $order->cat_ID ] = $order->postcat_order;
            }
        }
    }


   
/**
     * Get item order per category
     *
     * @param integer Category ID, NULL - for main category
     * @return double|NULL Order or NULL if an order is not defined for requested category
     */
   
function get_order( $cat_ID = NULL )
    {
       
$this->load_orders();

        if(
$cat_ID === NULL )
        {    
// Use main category:
           
global $Blog;
            if( isset(
$Blog ) &&
                ! empty(
$this->orders_per_coll[ $Blog->ID ] ) &&
               
$Blog->ID != $this->get_blog_ID() )
            {    
// Use sum of post orders from categories of cross-posted collection,
                // if current collection is a collection of extra category of this Item:
               
$orders_sum = NULL;
                foreach(
$this->orders_per_coll[ $Blog->ID ] as $extra_cat_order )
                {
                    if(
$extra_cat_order !== NULL )
                    {
                       
$orders_sum += $extra_cat_order;
                    }
                }
                return
$orders_sum;
            }
            else
            {    
// Use order of main category,
                // If current collection is same as collection of main category:
               
$cat_ID = $this->get( 'main_cat_ID' );
            }
        }

        return isset(
$this->orders[ $cat_ID ] ) ? $this->orders[ $cat_ID ] : NULL;
    }


   
/**
     * Get item order per category by requested collection ID
     *
     * @param integer Collection ID
     * @param boolean TRUE to exclude NULL orders from result
     * @return array Array of orders (Key - Category ID, Value - Item's order)
     */
   
function get_orders_by_coll_ID( $coll_ID, $exclude_null_orders = false )
    {
       
$this->load_orders();

        if( isset(
$this->orders_per_coll[ $coll_ID ] ) )
        {
           
$orders_per_coll = $this->orders_per_coll[ $coll_ID ];
            if(
$exclude_null_orders )
            {    
// Exclude NULL orders:
               
foreach( $orders_per_coll as $order_cat_ID => $order )
                {
                    if(
$order === NULL )
                    {
                        unset(
$orders_per_coll[ $order_cat_ID ] );
                    }
                }
            }
        }
        else
        {    
// No orders for the requested collection:
           
$orders_per_coll = array();
        }

        return
$orders_per_coll;
    }


   
/**
     * Update item order per category
     *
     * @param double New order value
     * @param integer Category ID, NULL - for main category or for extra category from provided Collection ID
     * @param integer Collection ID - to use extra category when $cat_ID is NULL
     * @return boolean
     */
   
function update_order( $order, $cat_ID = NULL, $coll_ID = NULL )
    {
        global
$DB;

        if( empty(
$this->ID ) )
        {    
// Item must be created:
           
return false;
        }

        if(
$cat_ID === NULL )
        {    
// Find what category to use for updating of order:
           
if( empty( $coll_ID ) || $this->get_blog_ID() == $coll_ID )
            {    
// Use main category:
               
$cat_ID = $this->get( 'main_cat_ID' );
            }
            elseif(
count( $this->get_orders_by_coll_ID( $coll_ID ) ) == 1 )
            {    
// Use extra category if it is single category per Collection for this Item:
               
$extra_cats = array_keys( $this->orders_per_coll[ $coll_ID ] );
               
$cat_ID = $extra_cats[0];
            }
        }

        if( empty(
$cat_ID ) )
        {    
// Don't try to update without provided and detected Category:
           
return false;
        }

       
// Change order to correct value:
       
$order = ( $order === '' || $order === NULL ? NULL : floatval( $order ) );

       
// Insert/Update order per category:
       
$r = $DB->query( 'REPLACE INTO T_postcats ( postcat_post_ID, postcat_cat_ID, postcat_order )
            VALUES ( '
.$this->ID.', '.intval( $cat_ID ).', '.$DB->quote( $order ).' ) ' );

        if(
$r )
        {    
// Update last touched date:
           
$this->update_last_touched_date();
        }

        return
$r;
    }


   
/**
     * Get param value from current revision
     *
     * @param mixed Name of parameter
     * @return mixed Value of parameter
     */
   
function get_from_revision( $parname )
    {
        if(
$this->is_revision() && // If revision is active currently for this Item
           
( $Revision = $this->get_revision() ) && // If current revision is detected for this Item
               
isset( $Revision->{'iver_'.$parname} ) ) // If revision really has the requested field
       
{    // Get value from current revision of this Item:
           
return $Revision->{'iver_'.$parname};
        }

       
// Get value from this Item:
       
return parent::get( $parname );
    }


   
/**
     * Assign the item to the first category we find in the requested collection
     *
     * @param integer $collection_ID
     */
   
function assign_to_first_cat_for_collection( $collection_ID )
    {
        global
$DB;

       
// Get the first category ID for the collection ID param
       
$cat_ID = $DB->get_var( '
                SELECT cat_ID
                    FROM T_categories
                 WHERE cat_blog_ID = '
.$collection_ID.'
                 ORDER BY cat_ID ASC
                 LIMIT 1'
);

       
// Set to the item the first category we got
       
$this->set( 'main_cat_ID', $cat_ID );
    }


   
/**
     * Get the list of renderers for this Item.
     * @return array
     */
   
function get_renderers()
    {
        return
explode( '.', $this->renderers );
    }


   
/**
     * Get the list of validated renderers for this Item. This includes stealth plugins etc.
     * @return array List of validated renderer codes
     */
   
function get_renderers_validated()
    {
        if( ! isset(
$this->renderers_validated) )
        {
            global
$Plugins;
           
$this->renderers_validated = $Plugins->validate_renderer_list( $this->get_renderers(), array( 'Item' => & $this ) );
        }
        return
$this->renderers_validated;
    }


   
/**
     * Add a renderer (by code) to the Item.
     * @param string Renderer code to add for this item
     * @return boolean True if renderers have changed
     */
   
function add_renderer( $renderer_code )
    {
       
$renderers = $this->get_renderers();
        if(
in_array( $renderer_code, $renderers ) )
        {
            return
false;
        }

       
$renderers[] = $renderer_code;
       
$this->set_renderers( $renderers );

       
$this->renderers_validated = NULL;
        return
true;
    }


   
/**
     * Remove a renderer (by code) from the Item.
     * @param string Renderer code to remove for this item
     * @return boolean True if renderers have changed
     */
   
function remove_renderer( $renderer_code )
    {
       
$r = false;
       
$renderers = $this->get_renderers();
        while( (
$key = array_search( $renderer_code, $renderers ) ) !== false )
        {
           
$r = true;
            unset(
$renderers[$key]);
        }

        if(
$r )
        {
           
$this->set_renderers( $renderers );
           
$this->renderers_validated = NULL;
           
//echo 'Removed renderer '.$renderer_code;
       
}
        return
$r;
    }


   
/**
     * Get the item tinyslug. If not exists -> create new
     *
     * @return string|boolean tinyslug on success, false otherwise
     */
   
function get_tinyslug()
    {
        global
$preview;

       
$tinyslug_ID = $this->tiny_slug_ID;
        if(
$tinyslug_ID != NULL )
        {
// the tiny slug for this item was already created
           
$SlugCache = & get_SlugCache();
           
$Slug = & $SlugCache->get_by_ID( $tinyslug_ID, false, false );
            return
$Slug === false ? false : $Slug->get( 'title' );
        }
        elseif( (
$this->ID > 0 ) && ( ! $preview ) )
        {
// create new tiny Slug for this item
            // Note: This may happen only in case of posts created before the tiny slug was introduced
           
global $DB;
           
load_funcs( 'slugs/model/_slug.funcs.php' );

           
$Slug = new Slug();
           
$Slug->set( 'title', getnext_tinyurl() );
           
$Slug->set( 'itm_ID', $this->ID );
           
$Slug->set( 'type', 'item' );
           
$DB->begin();
            if( !
$Slug->dbinsert() )
            {
// Slug dbinsert failed
               
$DB->rollback();
                return
false;
            }
           
$this->set( 'tiny_slug_ID', $Slug->ID );

           
// Update Item preserving mod date:
           
if( ! $this->dbupdate( false ) )
            {
// Item dbupdate failed
               
$DB->rollback();
                return
false;
            }
           
$DB->commit();

           
// update last tinyurl value on database
            // Note: This doesn't have to be part of the above transaction, no problem if it doesn't succeed to update, or if override a previously updated value.
           
global $Settings;
           
$Settings->set( 'tinyurl', $Slug->get( 'title' ) );
           
$Settings->dbupdate();

            return
$Slug->get( 'title' );
        }

        return
false;
    }


   
/**
     * Get all slugs of this Item, except of tiny slug
     *
     * @param string|NULL Separator, NULL - to return array
     * @return string Slugs list
     */
   
function get_slugs( $separator = ', ' )
    {
        if( ! isset(
$this->slugs ) )
        {    
// Initialize item slugs:
           
if( empty( $this->ID ) )
            {    
// Get creating Item:
                //return $this->get( 'urltitle' );
               
$this->slugs = array();
            }
            else
            {    
// Load slugs from DB once:
               
global $DB;
               
$SQL = new SQL( 'Get slugs of the Item #'.$this->ID );
               
$SQL->SELECT( 'slug_title, IF( slug_ID = '.intval( $this->canonical_slug_ID ).', 0, slug_ID ) AS slug_order_num' );
               
$SQL->FROM( 'T_slug' );
               
$SQL->WHERE( 'slug_itm_ID = '.$DB->quote( $this->ID ) );
                if( ! empty(
$this->tiny_slug_ID ) )
                {    
// Exclude tiny slug from list:
                   
$SQL->WHERE_and( 'slug_ID != '.$DB->quote( $this->tiny_slug_ID ) );
                }
               
$SQL->ORDER_BY( 'slug_order_num' );
               
$this->slugs = $DB->get_col( $SQL );
            }
        }

        return
$separator === NULL ? $this->slugs : implode( $separator, $this->slugs );
    }


   
/**
     * Get the item tiny url
     * @return string the tiny url on success, empty string otherwise
     */
   
function get_tinyurl( $use_tinyslug = true )
    {
        if(
$use_tinyslug )
        {
            if( (
$slug = $this->get_tinyslug() ) == false )
            {
                return
'';
            }
        }
        else
        {
           
$slug = $this->urltitle;
        }

       
$Collection = $Blog = & $this->get_Blog();
        if( (
$Blog->get_setting('tinyurl_type') == 'advanced' ) && ( $tinyurl_domain = $Blog->get_setting('tinyurl_domain') ) )
        {
            return
url_add_tail( $tinyurl_domain, '/'.$slug );
        }
        else
        {
            return
url_add_tail( $Blog->get( 'url'), '/'.$slug );
        }

    }


   
/**
     * Create and return the item tinyurl link.
     *
     * @param array Params:
     *  - 'before': to display before link
     *  - 'after': to display after link
     *  - 'text': link text
     *  - 'title': link title
     *  - 'class': class name
     *  - 'style': link style
     * @return string the tinyurl link on success, empty string otherwise
     */
   
function get_tinyurl_link( $params = array() )
    {
       
// Make sure we are not missing any param:
       
$params = array_merge( array(
               
'before'       => ' ',
               
'after'        => ' ',
               
'text'         => '#',
               
'title'        => '#',
               
'class'        => '',
               
'style'           => '',
               
'use_tinyslug' => true,
            ),
$params );

        if(
$params['use_tinyslug'] )
        {
            if( (
$slug = $this->get_tinyslug() ) == false )
            {
                return
'';
            }
        }
        else
        {
           
$slug = $this->urltitle;
        }

        if( !
$this->ID )
        {
// preview..
           
return false;
        }

        if(
$params['title'] == '#' )
        {
           
$params['title'] = T_( 'This is a tinyurl you can copy/paste into twitter, emails and other places where you need a short link to this post' );
        }
        if(
$params['text'] == '#' )
        {
           
$params['text'] = $slug;
        }

       
$actionurl = $this->get_tinyurl( $params['use_tinyslug'] );

       
$r = $params['before'];
       
$r .= '<a href="'.$actionurl;
       
$r .= '" title="'.$params['title'].'"';
        if( !empty(
$params['class'] ) ) $r .= ' class="'.$params['class'].'"';
        if( !empty(
$params['style'] ) ) $r .= ' style="'.$params['style'].'"';
       
$r .=  '>'.$params['text'].'</a>';
       
$r .= $params['after'];

        return
$r;
    }


   
/**
     * Display the item tinyurl link
     */
   
function tinyurl_link( $params = array() )
    {
        echo
$this->get_tinyurl_link( $params );
    }


   
/**
     * Get an url to this item
     * @param string values:
     *         - 'admin_view': url to this item admin interface view
     *         - 'public_view': url to this item public interface view (permanent url)
     *         - 'edit': url to this item edit screen
     * @return string the url if exists, empty string otherwise
     */
   
function get_url( $type )
    {
        global
$admin_url;
        switch(
$type )
        {
            case
'admin_view':
                return
$admin_url.'?ctrl=items&amp;blog='.$this->get_blog_ID().'&amp;p='.$this->ID;
            case
'public_view':
                return
$this->get_permanent_url();
            case
'edit':
                return
$this->get_edit_url();
            default:
                return
'';
        }
    }


   
/**
     * Get the number of comments on this item
     *
     * @param string the status of counted comments
     * @return integer the number of comments
     */
   
function get_number_of_comments( $status = NULL )
    {
        global
$DB;

       
$sql = 'SELECT count( comment_ID )
                FROM T_comments
                WHERE comment_item_ID = '
.$this->ID;

        if(
$status != NULL )
        {
           
$sql .= ' AND comment_status = "'.$status.'"';
        }

        return
$DB->get_var( $sql );
    }


   
/**
     * Get the latest Comment on this Item
     *
     * @param array|NULL Restrict comments selection with statuses, NULL - to select only allowed statuses for current User
     * @param string Type of the latest comment: NULL|'date' - latest added comment, 'last_touched_ts' - latest touched comment
     * @return Comment
     */
   
function & get_latest_Comment( $statuses = NULL, $order_date_type = NULL )
    {
        global
$DB;

        if(
$this->latest_Comment === NULL )
        {
            if( empty(
$this->ID ) )
            {    
// New item has no comments:
               
$this->latest_Comment = false;
                return
$this->latest_Comment;
            }

           
$SQL = new SQL( 'Get the latest Comment on the Item #'.$this->ID );
           
$SQL->SELECT( 'comment_ID' );
           
$SQL->FROM( 'T_comments' );
           
$SQL->WHERE( 'comment_item_ID = '.$DB->quote( $this->ID ) );
           
$SQL->WHERE_and( 'comment_type != "meta"' );
            if(
$statuses === NULL )
            {    
// Restrict with comment statuses which are allowed for current User:
               
$SQL->WHERE_and( statuses_where_clause( get_inskin_statuses( $this->get_blog_ID(), 'comment' ), 'comment_', $this->get_blog_ID(), 'blog_comment!', true ) );
            }
            elseif(
is_array( $statuses ) && count( $statuses ) )
            {    
// Restrict with given comment statuses:
               
$SQL->WHERE_and( 'comment_status IN ( '.$DB->quote( $statuses ).' )' );
            }
            if(
$order_date_type == 'last_touched_ts' )
            {    
// Get the latest touched comment:
               
$SQL->ORDER_BY( 'comment_last_touched_ts DESC, comment_ID DESC' );
            }
            else
            {    
// Get the latest added comment:
               
$SQL->ORDER_BY( 'comment_date DESC, comment_ID DESC' );
            }
           
$SQL->LIMIT( '1' );

            if(
$comment_ID = $DB->get_var( $SQL ) )
            {    
// Load the latest Comment in cache:
               
$CommentCache = & get_CommentCache();
               
// WARNING: Do NOT get this object by reference because it may rewrites current updating Comment:
               
$this->latest_Comment = $CommentCache->get_by_ID( $comment_ID );
            }
            else
            {    
// Set FALSE to don't call SQL query twice when the item has no comments yet:
               
$this->latest_Comment = false;
            }
        }

        return
$this->latest_Comment;
    }


   
/**
     * Get the ratings of comments on this item
     *
     * @retrun array of [ ratings, active ratings ] for this comment
     */
   
function get_ratings()
    {
        global
$DB, $localtimenow;

       
$this->load_Blog();

       
// Count each published comments rating grouped by active/expired status and by rating value:
       
$SQL = new SQL( 'Count each published comments rating grouped by active/expired status and by rating value' );
       
$SQL->SELECT( 'comment_rating, count( comment_ID ) AS cnt,' );
       
$SQL->SELECT_add( 'IF( iset_value IS NULL OR iset_value = "" OR TIMESTAMPDIFF(SECOND, comment_date, '.$DB->quote( date2mysql( $localtimenow ) ).') < iset_value, "active", "expired" ) as expiry_status' );
       
$SQL->FROM( 'T_comments' );
       
$SQL->FROM_add( 'LEFT JOIN T_items__item_settings ON iset_item_ID = comment_item_ID AND iset_name = "comment_expiry_delay"' );
       
$SQL->WHERE( 'comment_item_ID = '.$this->ID );
       
$SQL->WHERE_and( statuses_where_clause( get_inskin_statuses( $this->Blog->ID, 'comment' ), 'comment_', $this->Blog->ID, 'blog_comment!' ) );
       
$SQL->GROUP_BY( 'expiry_status, comment_rating' );
       
$SQL->ORDER_BY( 'comment_rating DESC' );
       
$results = $DB->get_results( $SQL );

       
// init rating arrays
       
$ratings = array();
       
$ratings['total'] = 0;
       
$ratings['summary'] = 0;
       
$ratings['unrated'] = 0;
       
$active_ratings = array();
       
$active_ratings['total'] = 0;
       
$active_ratings['summary'] = 0;
       
$active_ratings['unrated'] = 0;

        if( empty(
$results ) )
        {
// No rating at all
           
$ratings['all_ratings'] = 0;
           
$active_ratings['all_ratings'] = 0;
            return array(
$ratings, $active_ratings );
        }

       
// Init all ratings count to 0
       
for( $i=5; $i>=1; $i-- )
        {
           
$ratings[$i] = 0;
           
$active_ratings[$i] = 0;
        }

       
// Count active and overall rating values
       
foreach( $results as $rating )
        {
           
$index = ( $rating->comment_rating == 0 ) ? 'unrated' : $rating->comment_rating;
           
$ratings[$index] += $rating->cnt;
           
$ratings['total'] += $rating->cnt;
           
$ratings['summary'] += ( $rating->cnt * $rating->comment_rating );
            if(
$rating->expiry_status == 'active' )
            {
// this rating is not expired yet
               
$active_ratings[$index] = $rating->cnt;
               
$active_ratings['total'] += $rating->cnt;
               
$active_ratings['summary'] += ( $rating->cnt * $rating->comment_rating );
            }
        }

       
$ratings['all_ratings'] = $ratings['total'] - $ratings['unrated'];
       
$active_ratings['all_ratings'] = $active_ratings['total'] - $active_ratings['unrated'];

        return array(
$ratings, $active_ratings );
    }


     
/**
     * Get a setting.
     *
     * @return string|false|NULL value as string on success; NULL if not found; false in case of error
     */
   
function get_setting( $parname )
    {
       
$this->load_ItemSettings();

        return
$this->ItemSettings->get( $this->ID, $parname );
    }


   
/**
     * Set a setting.
     *
     * @return boolean true, if the value has been set, false if it has not changed.
     */
   
function set_setting( $parname, $value, $make_null = false )
    {
       
// Make sure item settings are loaded
       
$this->load_ItemSettings();

        if(
$make_null && empty($value) )
        {
           
$value = NULL;
        }

        return
$this->ItemSettings->set( $this->ID, $parname, $value );
    }


   
/**
     * Delete a setting.
     *
     * @return boolean true, if the value has been set, false if it has not changed.
     */
   
function delete_setting( $parname )
    {
         
// Make sure item settings are loaded
       
$this->load_ItemSettings();

        return
$this->ItemSettings->delete( $this->ID, $parname );
    }


   
/**
     * Make sure item settings are loaded.
     */
   
function load_ItemSettings()
    {
        if( ! isset(
$this->ItemSettings) )
        {
           
load_class( 'items/model/_itemsettings.class.php', 'ItemSettings' );
           
$this->ItemSettings = new ItemSettings();
        }
    }


   
/**
     * Get location of current Item
     *
     * @param string Text before location
     * @param string Text after location
     * @param string Separator
     */
   
function get_location( $before, $after, $separator = ', ' )
    {
       
$location = array();
       
$location[] = $this->get_city();
       
$location[] = $this->get_subregion();
       
$location[] = $this->get_region();
       
$location[] = $this->get_country();

       
// Delete empty elements
       
$location = array_filter($location);

       
$r = '';

        if( !empty(
$location ) )
        {    
// Display location
           
$r .= $before;

           
$r .= implode( $separator, $location );

           
$r .= $after;
        }

        return
$r;
    }


   
/**
     * Display location of current Item
     *
     * @param string Text before location
     * @param string Text after location
     * @param string Separator
     */
   
function location( $before, $after, $separator = ', ' )
    {
        echo
$this->get_location( $before, $after, $separator );
    }


   
/**
     * Get country of current Item
     *
     * @param array params
     * @return string Country name
     */
   
function get_country( $params = array() )
    {
       
// Make sure we are not missing any param:
       
$params = array_merge( array(
               
'before' => '',
               
'after'  => '',
            ),
$params );

       
$this->load_Blog();
        if(
$this->ctry_ID == 0 || ! $this->country_visible() )
        {    
// Country is not defined for current Item OR Counries are hidden
           
return;
        }

       
load_class( 'regional/model/_country.class.php', 'Country' );
       
$CountryCache = & get_CountryCache();

        if(
$Country = $CountryCache->get_by_ID( $this->ctry_ID, false, false ) )
        {    
// Display country name
           
$result = $params['before'];

           
$result .= $Country->get_name();

           
$result .= $params['after'];

            return
$result;
        }
    }


   
/**
     * Get region of current Item
     *
     * @param array params
     * @return string Region name
     */
   
function get_region( $params = array() )
    {
       
// Make sure we are not missing any param:
       
$params = array_merge( array(
               
'before' => '',
               
'after'  => '',
            ),
$params );

       
$this->load_Blog();
        if(
$this->rgn_ID == 0 || ! $this->region_visible() )
        {    
// Region is not defined for current Item
           
return;
        }

       
load_class( 'regional/model/_region.class.php', 'Region' );
       
$RegionCache = & get_RegionCache();

        if(
$Region = $RegionCache->get_by_ID( $this->rgn_ID, false, false ) )
        {    
// Display region name
           
$result = $params['before'];

           
$result .= $Region->get_name();

           
$result .= $params['after'];

            return
$result;
        }
    }


   
/**
     * Get subregion of current Item
     *
     * @param array params
     * @return string Subregion name
     */
   
function get_subregion( $params = array() )
    {
       
// Make sure we are not missing any param:
       
$params = array_merge( array(
               
'before' => '',
               
'after'  => '',
            ),
$params );

       
$this->load_Blog();
        if(
$this->subrg_ID == 0 || ! $this->subregion_visible() )
        {    
// Subregion is not defined for current Item
           
return;
        }

       
load_class( 'regional/model/_subregion.class.php', 'Subregion' );
       
$SubregionCache = & get_SubregionCache();

        if(
$Subregion = $SubregionCache->get_by_ID( $this->subrg_ID, false, false ) )
        {    
// Display subregion name
           
$result = $params['before'];

           
$result .= $Subregion->get_name();

           
$result .= $params['after'];

            return
$result;
        }
    }


   
/**
     * Get city of current Item
     *
     * @param array params
     * @return string City name + postcode
     */
   
function get_city( $params = array() )
    {
       
// Make sure we are not missing any param:
       
$params = array_merge( array(
               
'before' => '',
               
'after'  => '',
               
'template' => '$name$ ($postcode$)', // $name$ - City name; $postcode$ - City postcode
           
), $params );

       
$this->load_Blog();
        if(
$this->city_ID == 0 || ! $this->city_visible() )
        {    
// City is not defined for current Item
           
return;
        }

       
load_class( 'regional/model/_city.class.php', 'City' );
       
$CityCache = & get_CityCache();

        if(
$City = $CityCache->get_by_ID( $this->city_ID, false, false ) )
        {    
// Display city info
           
$result = $params['before'];

           
$city_tamplates = array( '$name$', '$postcode$' );
           
$city_data = array( $City->get_name(), $City->get_postcode() );
           
$result .= str_replace( $city_tamplates, $city_data, $params['template'] );

           
$result .= $params['after'];

            return
$result;
        }
    }


   
/**
     * Get item revision
     *
     * @param string Revision ID with prefix as first char: 'a'(or digit) - archived version, 'c' - current version, 'p' - proposed change, NULL - to use current revision
     * @return object Revision
     */
   
function & get_revision( $iver_ID = NULL )
    {
        global
$DB;

        if(
$iver_ID === NULL && $this->is_revision() )
        {    
// Use current revision
           
$iver_ID = $this->revision;
        }

        if( empty(
$this->ID ) || empty( $iver_ID ) )
        {    
// Item must be stored in DB to get revisions and Revision ID must be defined:
           
$r = false;
            return
$r;
        }

        if( isset(
$this->revisions[ $iver_ID ] ) )
        {    
// Get revision data from cached array:
           
return $this->revisions[ $iver_ID ];
        }

       
// Save original ID to use this for storing in cache:
       
$orig_iver_ID = $iver_ID;

        if(
$iver_ID == 'last_archived' || $iver_ID == 'last_proposed' )
        {    
// Get last archived version or last proposed change:
           
$iver_type = substr( $iver_ID, 5 );
           
$revision_SQL = new SQL( 'Get '.str_replace( '_', ' ', $iver_ID ).' version of the Item #'.$this->ID );
           
$revision_SQL->SELECT( '*, CONCAT( "'.( $iver_type == 'archived' ? 'a' : 'p' ).'", iver_ID ) AS param_ID' );
           
$revision_SQL->FROM( 'T_items__version' );
           
$revision_SQL->WHERE( 'iver_itm_ID = '.$DB->quote( $this->ID ) );
           
$revision_SQL->WHERE_and( 'iver_type = '.$DB->quote( $iver_type ) );
           
$revision_SQL->ORDER_BY( 'iver_ID DESC' );
           
$revision_SQL->LIMIT( '1' );
           
$this->revisions[ $orig_iver_ID ] = $DB->get_row( $revision_SQL );

            return
$this->revisions[ $orig_iver_ID ];
        }

       
// Get version type from first char:
       
$iver_ID_len = strlen( $iver_ID );
       
$iver_type = $iver_ID_len > 0 ? substr( $iver_ID, 0, 1 ) : 'c';

        if(
intval( $iver_type ) == 0 )
        {    
// Extract version ID:
           
$iver_ID = intval( substr( $iver_ID, 1 ) );
        }
        else
        {    
// Use the provided integer ID and decide this as archived version:
           
$iver_type = 'a';
        }
       
$iver_ID = intval( $iver_ID );
        if(
$iver_ID == 0 )
        {    
// Force to get current version for request without ID:
           
$iver_type = 'c';
        }

        switch(
$iver_type )
        {
            case
'a':
            case
'p':
               
// Archived version or Proposed change:
               
$revision_SQL = new SQL( 'Get '.( $iver_type == 'a' ? 'an archived version' : 'a proposed change' ).' #'.$this->ID.' for Item #'.$this->ID );
               
$revision_SQL->SELECT( '*, CONCAT( "'.$iver_type.'", iver_ID ) AS param_ID' );
               
$revision_SQL->FROM( 'T_items__version' );
               
$revision_SQL->WHERE( 'iver_ID = '.$DB->quote( $iver_ID ) );
               
$revision_SQL->WHERE_and( 'iver_itm_ID = '.$DB->quote( $this->ID ) );
               
$revision_SQL->WHERE_and( 'iver_type = "'.( $iver_type == 'a' ? 'archived' : 'proposed' ).'"' );
               
$this->revisions[ $orig_iver_ID ] = $DB->get_row( $revision_SQL->get(), OBJECT, NULL, $revision_SQL->title );
                break;

            case
'c':
            default:
               
// Current version:
               
$this->revisions[ $orig_iver_ID ] = (object) array(
                   
'param_ID'           => 'c',
                   
'iver_ID'            => 0,
                   
'iver_type'          => 'current',
                   
'iver_itm_ID'        => $this->ID,
                   
'iver_edit_last_touched_ts' => $this->contents_last_updated_ts,
                   
'iver_edit_user_ID'  => $this->lastedit_user_ID,
                   
'iver_status'        => $this->status,
                   
'iver_title'         => $this->title,
                   
'iver_content'       => $this->content
               
);
                break;
        }

        return
$this->revisions[ $orig_iver_ID ];
    }


   
/**
     * Clear revision and item's data to current version
     */
   
function clear_revision()
    {
       
// Reset this Item in order to use current title, content and other data:
       
$ItemCache = get_ItemCache();
        unset(
$ItemCache->cache[ $this->ID ] );
        if(
$current_Item = $ItemCache->get_by_ID( $this->ID, false, false ) )
        {    
// Revert fields to current values:
           
$revision_fields = array( 'title', 'content', 'status' );
            foreach(
$revision_fields as $revision_field )
            {
               
$this->set( $revision_field, $current_Item->get( $revision_field ) );
            }
        }

        if( isset(
$this->revision ) )
        {    
// Reset to use data from current Item:
           
unset( $this->revision );
        }
    }


   
/**
     * Get item custom field value by index from current revision
     *
     * @param string Field name, see {@link load_custom_field_value()}
     * @return mixed false if the field doesn't exist Double/String otherwise depending from the custom field type
     */
   
function get_revision_custom_field_value( $field_name )
    {
        if( !
$this->is_revision() )
        {    
// Revision is not active:
           
return NULL;
        }

        if( ! (
$Revision = & $this->get_revision() ) )
        {    
// Revision cannot be detected:
           
return NULL;
        }

        if( ! isset(
$Revision->custom_fields ) )
        {    
// Load custom fields from DB and store in cache:
           
global $DB;
           
$SQL = new SQL( 'Get custom fields values of Item #'.$this->ID.' for revision #'.$Revision->iver_ID.'('.$Revision->iver_type.')' );
           
$SQL->SELECT( 'IFNULL( itcf_name, CONCAT( "!deleted_", ivcf_itcf_ID ) ), ivcf_value' );
           
$SQL->FROM( 'T_items__version_custom_field' );
           
$SQL->FROM_add( 'LEFT JOIN T_items__type_custom_field ON ivcf_itcf_ID = itcf_ID' );
           
$SQL->WHERE_and( 'ivcf_iver_ID = '.$DB->quote( $Revision->iver_ID ) );
           
$SQL->WHERE_and( 'ivcf_iver_type = '.$DB->quote( $Revision->iver_type ) );
           
$SQL->WHERE_and( 'ivcf_iver_itm_ID = '.$DB->quote( $Revision->iver_itm_ID ) );
           
$Revision->custom_fields = $DB->get_assoc( $SQL );
        }

        if( isset(
$Revision->custom_fields[ $field_name ] ) )
        {    
// If the revision has a requested custom field:
           
return $Revision->custom_fields[ $field_name ];
        }
        else
        {    
// If the revision has no requested custom field:
           
return NULL;
        }
    }


   
/**
     * Update this item from revision
     *
     * @param string Revision ID with prefix as first char: 'a'(or digit) - archived version, 'c' - current version, 'p' - proposed change, NULL - to use current revision
     * @return boolean TRUE on success, FALSE on failed
     */
   
function update_from_revision( $iver_ID = NULL )
    {
        if( ! (
$Revision = & $this->get_revision( $iver_ID ) ) )
        {    
// If revision is not found:
           
return false;
        }

        global
$DB;

       
$DB->begin( 'SERIALIZABLE' );

       
// Update main fields:
       
$this->set( 'status', $Revision->iver_status );
       
$this->set( 'title', $Revision->iver_title );
       
$this->set( 'content', $Revision->iver_content );

       
$custom_fields = $this->get_type_custom_fields();
        if(
count( $custom_fields ) )
        {    
// Update custom fields if item type has them:
            // Switch to the requested revision:
           
$this->set( 'revision', $Revision->param_ID );
            foreach(
$custom_fields as $custom_field )
            {
               
$this->set_custom_field( $custom_field['name'], $this->get_custom_field_value( $custom_field['name'] ) );
            }
        }

        if(
$Revision->iver_type == 'proposed' )
        {    
// Update last updated data from proposed change:
           
$this->set( 'lastedit_user_ID', $Revision->iver_edit_user_ID );
           
$this->set( 'datemodified', $Revision->iver_edit_last_touched_ts );
           
$this->set( 'contents_last_updated_ts', $Revision->iver_edit_last_touched_ts );
           
$this->set_last_touched_ts();
           
// Don't auto track date fields on accepting of proposed change:
           
$auto_track_modification = false;
           
// Force to create new revision even if no 90 seconds after last changing:
           
$force_create_revision = true;
        }
        else
        {    
// Auto track date fields on restoring from history:
           
$auto_track_modification = true;
           
// Don't force creating of new revision:
           
$force_create_revision = false;
        }

       
$r = $this->dbupdate( $auto_track_modification, true, true, $force_create_revision );

       
// Update attachments:
       
$current_links_SQL = new SQL( 'Get current links of Item #'.$this->ID.' before updating from revision' );
       
$current_links_SQL->SELECT( '*' );
       
$current_links_SQL->FROM( 'T_links' );
       
$current_links_SQL->WHERE( 'link_itm_ID = '.$this->ID );
       
$current_links = $DB->get_results( $current_links_SQL, OBJECT, '', 'link_ID' );

       
$revision_links_SQL = new SQL( 'Get attachments of Item #'.$this->ID.' for revision #'.$Revision->iver_ID.'('.$Revision->iver_type.') before updating from revision' );
       
$revision_links_SQL->SELECT( 'T_items__version_link.*' );
       
$revision_links_SQL->FROM( 'T_items__version_link' );
       
$revision_links_SQL->FROM_add( 'INNER JOIN T_files ON ivl_file_ID = file_ID' );
       
$revision_links_SQL->WHERE( 'ivl_iver_ID = '.$Revision->iver_ID );
       
$revision_links_SQL->WHERE_and( 'ivl_iver_itm_ID = '.$this->ID );
       
$revision_links_SQL->WHERE_and( 'ivl_iver_type = '.$DB->quote( $Revision->iver_type ) );
       
$revision_links = $DB->get_results( $revision_links_SQL, OBJECT, '', 'ivl_link_ID' );

       
// Use this array to check for uniqueness links by order:
       
$link_orders = array();

       
$LinkOwner = new LinkItem( $this );
       
$LinkCache = & get_LinkCache();
        foreach(
$current_links as $current_link_ID => $current_link )
        {
            if( ! isset(
$revision_links[ $current_link_ID ] ) )
            {    
// This link doesn't exist in revision, Delete it:
               
if( $deleted_Link = & $LinkCache->get_by_ID( $current_link_ID, false, false ) &&
                   
$LinkOwner->remove_link( $deleted_Link, true ) )
                {
                   
$LinkOwner->after_unlink_action( $current_link_ID );
                }
            }
            else
            {    
// This link exists in revision, we should keep it:
                // Also store an order in the array to check for uniqueness them by order:
               
$link_orders[ $current_link_ID ] = $current_link->link_order;
            }
        }

        if(
count( $revision_links ) )
        {    
// It revision has at least one link/attachment:
           
$updated_links = array();
           
$delete_updated_links = array();
            foreach(
$revision_links as $revision_link_ID => $revision_link )
            {    
// Add links of revision to array to check for uniqueness them by order:
               
$link_orders[ $revision_link_ID ] = $revision_link->ivl_order;
                if( isset(
$current_links[ $revision_link_ID ] ) )
                {    
// Store here what links of current version must be updated from revision:
                   
$updated_links[ $revision_link_ID ] = $current_links[ $revision_link_ID ];
                   
$delete_updated_links[] = $revision_link_ID;
                }
            }

            if(
count( $delete_updated_links ) )
            {    
// Delete the links(which must be updated) rows temporary from DB in order insert them later with new data to avoid duplicate entry error:
               
$DB->query( 'DELETE FROM T_links
                    WHERE link_ID IN ( '
.$DB->quote( $delete_updated_links ).' )
                        AND link_itm_ID = '
.$DB->quote( $this->ID ),
                   
'Temporary deleting of links rows which must be updated from revision' );
            }

           
// Sort links by order and check for duplicate orders:
           
asort( $link_orders );
           
$prev_order = -1;
            foreach(
$link_orders as $link_ID => $link_order )
            {
                if(
$prev_order >= $link_order )
                {    
// If previous link has same order then increase it to avoid a duplicate error on insert links in DB:
                   
$link_orders[ $link_ID ]++;
                }
               
$prev_order = $link_order;
            }

           
$insert_links_sql = array();
            foreach(
$revision_links as $revision_link_ID => $revision_link )
            {
                if( isset(
$updated_links[ $revision_link_ID ] ) )
                {    
// This link exists in current version, Update it:
                   
$updated_link = $updated_links[ $revision_link_ID ];
                }
                {    
// This link doesn't exist in current version, Insert it:
                   
$updated_link = false;
                }
               
$insert_links_sql[] = '( '.$DB->quote( array(
                       
$revision_link_ID,
                       
$updated_link ? $updated_link->link_datecreated      : $Revision->iver_edit_last_touched_ts,
                       
$updated_link ? $updated_link->link_datemodified     : $Revision->iver_edit_last_touched_ts,
                       
$updated_link ? $updated_link->link_creator_user_ID  : $Revision->iver_edit_user_ID,
                       
$updated_link ? $updated_link->link_lastedit_user_ID : $Revision->iver_edit_user_ID,
                       
$this->ID,
                       
$revision_link->ivl_file_ID,
                       
$revision_link->ivl_position,
                       
$link_orders[ $revision_link_ID ]
                    ) ).
' )';
            }

           
// Insert/Update links with data from revision:
           
$DB->query( 'INSERT INTO T_links ( link_ID, link_datecreated, link_datemodified, link_creator_user_ID, link_lastedit_user_ID, link_itm_ID, link_file_ID, link_position, link_order )
                VALUES '
.implode( ', ', $insert_links_sql ),
               
'Insert/Update item links with data from revision #'.$Revision->iver_ID.'('.$Revision->iver_type.')' );
        }

       
$DB->commit();

        return
$r;
    }


   
/**
     * Delete proposed changes of this Item from DB
     *
     * @param string Action 'accept', 'reject'
     * @param integer|string ID of the proposed change, 'last' to get last proposed change
     */
   
function clear_proposed_changes( $action = 'accept', $iver_ID = 'last' )
    {
        global
$DB;

        if( empty(
$this->ID ) )
        {    
// Item must be stored in nDB:
           
return;
        }

        if(
$iver_ID === 'last' )
        {    
// Try to get ID of the last proposed change:
           
if( $last_proposed_Revision = $this->get_revision( 'last_proposed' ) )
            {    
// If this Item has at least one proposed change:
               
$iver_ID = $last_proposed_Revision->iver_ID;
            }
            else
            {    
// This Item has no proposed changes:
               
return;
            }

        }

        if(
strpos( $action, 'accept' ) !== false )
        {    
// Accept(delete) all previous proposed changes:
           
$delete_direction = '<=';
        }
        elseif(
strpos( $action, 'reject' ) !== false )
        {    
// Reject(delete) all newer proposed changes:
           
$delete_direction = '>=';
        }
        else
        {
           
debug_die( 'Unhandled action "'.$action.'" to clear proposed changes!' );
        }

       
// Clear proposed changes:
       
$DB->query( 'DELETE FROM T_items__version
            WHERE iver_itm_ID = '
.$DB->quote( $this->ID ).'
              AND iver_type = "proposed"
              AND iver_ID '
.$delete_direction.' '.$DB->quote( $iver_ID ) );
       
// Clear custom fields of the proposed changes:
       
$DB->query( 'DELETE FROM T_items__version_custom_field
            WHERE ivcf_iver_itm_ID = '
.$DB->quote( $this->ID ).'
              AND ivcf_iver_type = "proposed"
              AND ivcf_iver_ID '
.$delete_direction.' '.$DB->quote( $iver_ID ) );
       
// Clear links/attachments of the proposed changes:
       
$DB->query( 'DELETE FROM T_items__version_link
            WHERE ivl_iver_itm_ID = '
.$DB->quote( $this->ID ).'
              AND ivl_iver_type = "proposed"
              AND ivl_iver_ID '
.$delete_direction.' '.$DB->quote( $iver_ID ) );
    }


   
/**
     * Check if item is locked
     *
     * @return boolean TRUE - if item is locked
     */
   
function is_locked()
    {
        if( isset(
$this->is_locked ) )
        {
// item lock status was already set
           
return $this->is_locked;
        }

       
// Get item chapters to check lock status, but use cached chapters array instead of db query
       
$item_chapters = $this->get_Chapters();

        if(
count( $item_chapters ) )
        {
// Presuppose that all category is locked, we will change this value if only one category is not locked
           
$this->is_locked = true;
            foreach(
$item_chapters as $item_Chapter )
            {
// Check if all item categories is locked
               
if( !$item_Chapter->lock )
                {
// This category is not locked so the item is not locked either
                   
$this->is_locked = false;
                    break;
                }
            }
        }
        else
        {
// If no category was set yet ( e.g. in case of new item create ), the Item can't be locked
           
$this->is_locked = false;
        }

        return
$this->is_locked;
    }


   
/**
     * Set field last_touched_ts
     */
   
function set_last_touched_ts()
    {
        global
$localtimenow;

        if(
is_logged_in() )
        {
           
$this->load_content_read_status();
        }

       
$this->set_param( 'last_touched_ts', 'date', date2mysql( $localtimenow ) );
    }


   
/**
     * Set field contents_last_updated_ts
     */
   
function set_contents_last_updated_ts()
    {
        global
$localtimenow;

       
$this->set_param( 'contents_last_updated_ts', 'date', date2mysql( $localtimenow ) );
    }


   
/**
     * Update field last_touched_ts and parent categories
     *
     * @param boolean Use transaction
     * @param boolean Use TRUE to update item field last_touched_ts
     * @param boolean Use TRUE to update item field contents_last_updated_ts
     * @param boolean do we want to auto track the mod date?
     */
   
function update_last_touched_date( $use_transaction = true, $update_last_touched_ts = true, $update_contents_last_updated_ts = false, $auto_track_modification = false )
    {
        if(
$use_transaction )
        {
            global
$DB;
           
$DB->begin();
        }

        if(
$update_last_touched_ts || $update_contents_last_updated_ts )
        {    
// If at least one date field should be updated
           
if( $update_last_touched_ts )
            {    
// Update field last_touched_ts:
               
$this->set_last_touched_ts();
            }
            if(
$update_contents_last_updated_ts )
            {    
// Update field contents_last_updated_ts:
               
$this->set_contents_last_updated_ts();
            }
           
$this->dbupdate( $auto_track_modification, false, false );
        }

       
// Also update last touched date of all categories of this Item
       
$chapters = $this->get_Chapters();
        if(
count( $chapters ) > 0 )
        {
            foreach(
$chapters as $Chapter )
            {
               
$Chapter->update_last_touched_date();
                while(
$Chapter )
                {
// Update all parent chapters recursively
                   
$Chapter = $Chapter->get_parent_Chapter();
                    if( ! empty(
$Chapter ) )
                    {
                       
$Chapter->update_last_touched_date();
                    }
                }
            }
        }

        if(
$use_transaction )
        {
           
$DB->commit();
        }
    }


   
/**
     * Update field itud_read_item_ts for current User
     *
     * @param boolean TRUE to update a post read timestamp
     * @param boolean TRUE to update a comments read timestamp
     */
   
function update_read_timestamps( $read_post = true, $read_comments = true )
    {
        if( !
$read_post && ! $read_comments )
        {
// Nothing to update
           
return;
        }

        if(
$this->ID == 0 )
        {
// Item is not saved in DB
           
return;
        }

        if( !
is_logged_in() )
        {
// User is not logged in
           
return;
        }

       
$this->load_Blog();
        if( !
$this->Blog->get_setting( 'track_unread_content' ) )
        {    
// The tracking of unread content is turned off for the collection
           
return;
        }

        global
$DB, $current_User, $localtimenow;

       
$timestamp = date2mysql( $localtimenow );

       
$read_date = $this->get_user_data( 'item_date' );

        if(
$timestamp == $read_date )
        {    
// The read status is already updated, Don't repeat it:
           
return;
        }

        if( !
is_null( $read_date ) )
        {    
// Update the read status:
           
$update_fields = '';
            if(
$read_post )
            {    
// Update a post read timestamp:
               
$update_fields = 'itud_read_item_ts = '.$DB->quote( $timestamp );
            }
            if(
$read_comments )
            {    
// Update a comments read timestamp:
               
if( $read_post )
                {
                   
$update_fields .= ', ';
                }
               
$update_fields .= 'itud_read_comments_ts = '.$DB->quote( $timestamp );
            }
           
$DB->query( 'UPDATE T_items__user_data
                  SET '
.$update_fields.'
                WHERE itud_user_ID = '
.$DB->quote( $current_User->ID ).'
                  AND itud_item_ID = '
.$DB->quote( $this->ID ) );
        }
        else
        {    
// Insert new read status:
           
$insert_fields = '';
           
$insert_values = '';
            if(
$read_post )
            {    
// Update a post read timestamp:
               
$insert_fields = 'itud_read_item_ts';
               
$insert_values = $DB->quote( $timestamp );
            }
            if(
$read_comments )
            {    
// Update a comments read timestamp:
               
if( $read_post )
                {
                   
$insert_fields .= ', ';
                   
$insert_values .= ', ';
                }
               
$insert_fields .= 'itud_read_comments_ts';
               
$insert_values .= $DB->quote( $timestamp );
            }
           
$DB->query( 'INSERT INTO T_items__user_data ( itud_user_ID, itud_item_ID, '.$insert_fields.' )
                VALUES ( '
.$DB->quote( $current_User->ID ).', '.$DB->quote( $this->ID ).', '.$insert_values.' )' );
        }

       
// Update the cached item date:
       
$this->set_user_data( 'item_date', $timestamp );
    }


   
/**
     * Load timestamp when this post content was read by the current User
     */
   
function load_content_read_status()
    {
        if( !
is_logged_in() )
        {
            return;
        }

        if( !empty(
$this->content_read_status ) )
        {
            return;
        }

       
$this->content_read_status = $this->get_read_status();
    }


   
/**
     * Get the read status of this post and its comments for current User
     *
     * @return string 'read' - when current User already read this post
     *                'updated' - current user didn't read some new changes
     *                'new' - the post is new for current user
     */
   
function get_read_status()
    {
        if(
$this->ID == 0 )
        {
// Item is not saved in DB
           
return 'read';
        }

        if( !
is_logged_in() )
        {
// User is not logged in
           
return 'read';
        }

       
$this->load_Blog();
        if( !
$this->Blog->get_setting( 'track_unread_content' ) )
        {    
// The tracking of unread content is turned off for the collection
           
return 'read';
        }

        global
$DB;

       
$read_date = $this->get_user_data( 'item_date' );

        if( empty(
$read_date ) )
        {    
// This post is recent for current user
           
return 'new';
        }

       
// In theory, it would be more safe to use this comparison:
        // if( $read_date > $this->contents_last_updated_ts )
        // But until we have milli- or micro-second precision on timestamps, we decided it was a better trade-off to never see our own edits as unread. So we use:
       
if( $read_date >= $this->contents_last_updated_ts )
        {    
// This post was read by current user
           
return 'read';
        }

       
// This post is Unread by current user
       
return 'updated';
    }


   
/**
     * Get the user data from DB or from Cached array of this post for current User
     *
     * @param string|NULL Field name: 'item_date', 'comments_date', 'item_flag', NULL - to get all fields as array
     * @return array|string Array of all fields OR value of single field
     */
   
function get_user_data( $field = NULL )
    {
        global
$DB, $current_User, $cache_items_user_data;

        if( !
is_array( $cache_items_user_data ) )
        {    
// Init array first time:
           
$cache_items_user_data = array();
        }

        if( ! isset(
$cache_items_user_data[ $this->ID ] ) )
        {
// Get the read post date only one time from DB and store it in cache array
           
$SQL = new SQL( 'Get the data of item #'.$this->ID.' for user #'.$current_User->ID );
           
$SQL->SELECT( 'IFNULL( itud_read_item_ts, 0 ) AS item_date, IFNULL( itud_read_comments_ts, 0 ) AS comments_date, itud_flagged_item AS item_flag' );
           
$SQL->FROM( 'T_items__user_data' );
           
$SQL->WHERE( 'itud_user_ID = '.$DB->quote( $current_User->ID ) );
           
$SQL->WHERE_and( 'itud_item_ID = '.$DB->quote( $this->ID ) );
           
$cache_items_user_data[ $this->ID ] = $DB->get_row( $SQL, ARRAY_A );
        }

        if( isset(
$cache_items_user_data[ $this->ID ] ) && is_array( $cache_items_user_data[ $this->ID ] ) &&  empty( $cache_items_user_data[ $this->ID ] ) )
        {    
// Init empty user item data:
           
$cache_items_user_data[ $this->ID ] = NULL;
        }

        if(
$field === NULL )
        {    
// Return all fields as array:
           
return $cache_items_user_data[ $this->ID ];
        }
        else
        {    
// Return a value of single field:
           
return isset( $cache_items_user_data[ $this->ID ][ $field ] ) ? $cache_items_user_data[ $this->ID ][ $field ] : NULL;
        }
    }


   
/**
     * Set the user data to global cache array of this post for current User
     *
     * @param string Field name: 'item_date', 'comments_date', 'item_flag'
     * @param string Value
     */
   
function set_user_data( $field, $value )
    {
        global
$cache_items_user_data;

        if( ! isset(
$cache_items_user_data[ $this->ID ] ) || ! is_array( $cache_items_user_data[ $this->ID ] ) )
        {    
// Initialize array:
           
$cache_items_user_data[ $this->ID ] = array();
        }

       
$cache_items_user_data[ $this->ID ][ $field ] = $value;
    }


   
/**
     * Get a color read status icon if this post is unread by current User
     *
     * @param array Params
     * @return string
     */
   
function get_unread_status( $params = array() )
    {
       
$r = '';

       
$this->load_Blog();
        if( !
$this->Blog->get_setting( 'track_unread_content' ) )
        {    
// The tracking of unread content is turned off for the collection
           
return $r;
        }

       
// Set titles by Blog type:
       
$this->get_ItemType();
       
$title_new = $this->ItemType->get_item_denomination( 'title_new' );
       
$title_updated = $this->ItemType->get_item_denomination( 'title_updated' );

       
// Merge params
       
$params = array_merge( array(
               
'before'        => ' ',
               
'after'         => '',
               
'class'         => 'track_content',
               
'style'         => 'icon', // 'text'
               
'title_new'     => $title_new,
               
'title_updated' => $title_updated,
               
'title_read'    => T_('Read'),
               
'text_new'      => T_('New'),
               
'text_updated'  => T_('Updated'),
               
'text_read'     => T_('Read'),
               
'class_new'     => 'label label-warning',
               
'class_updated' => 'label label-danger',
               
'class_read'    => 'label label-success',
            ),
$params );

        switch(
$this->get_read_status() )
        {
            case
'new':
               
// This post is new for the current User, it was never opened
               
$r .= $params['before'];
                if(
$params['style'] == 'text' )
                {    
// Text style:
                   
$r .= '<span'
                       
.( empty( $params['class_new'] ) ? '' : ' class="'.$params['class_new'].'"')
                        .( empty(
$params['title_new'] ) ? '' : ' class="'.$params['title_new'].'"').'>'
                           
.$params['text_new']
                        .
'</span>';
                }
                else
                {    
// Icon style:
                   
$r .= get_icon( 'bullet_orange', 'imgtag', array( 'title' => $params['title_new'], 'class' => $params['class'] ) );
                }
               
$r .= $params['after'];
                break;

            case
'updated':
               
// The last updates of this post was not read by the current User
               
$r .= $params['before'];
                if(
$params['style'] == 'text' )
                {    
// Text style:
                   
$r .= '<span'
                       
.( empty( $params['class_updated'] ) ? '' : ' class="'.$params['class_updated'].'"')
                        .( empty(
$params['title_updated'] ) ? '' : ' class="'.$params['title_updated'].'"').'>'
                           
.$params['text_updated']
                        .
'</span>';
                }
                else
                {    
// Icon style:
                   
$r .= get_icon( 'bullet_brown', 'imgtag', array( 'title' => $params['title_updated'], 'class' => $params['class'] ) );
                }
               
$r .= $params['after'];
                break;

            case
'read':
            default:
               
// Don't display status icons if user already have read this post
               
if( $params['style'] == 'text' )
                {    
// Text style:
                   
$r .= $params['before'];
                   
$r .= '<span'
                       
.( empty( $params['class_read'] ) ? '' : ' class="'.$params['class_read'].'"')
                        .( empty(
$params['title_read'] ) ? '' : ' class="'.$params['title_read'].'"').'>'
                           
.$params['text_read']
                        .
'</span>';
                   
$r .= $params['after'];
                }
               
// No icon for read status.
               
break;
        }

        return
$r;
    }


   
/**
     * Display a color read status icon if this post is unread by current User
     *
     * @param array Params
     */
   
function display_unread_status( $params = array() )
    {
        echo
$this->get_unread_status( $params );
    }


   
/**
     * Check if item has a goal to insert a hit into DB
     *
     * @return boolean TRUE if goal hit was inser
     */
   
function check_goal()
    {
       
$goal_ID = $this->get_setting( 'goal_ID' );

        if( empty(
$goal_ID ) )
        {
// Item has no goal ID
           
return false;
        }

       
$GoalCache = & get_GoalCache();
        if( (
$Goal = $GoalCache->get_by_ID( $goal_ID, false, false ) ) === false )
        {
// Goal ID is incorrect
           
return false;
        }

        global
$Hit, $DB;

       
// We need to log the HIT now! Because we need the hit ID!
       
$Hit->log();

       
// Record a goal hit:
       
return $DB->query( 'INSERT INTO T_track__goalhit
                   ( ghit_goal_ID, ghit_hit_ID, ghit_params )
            VALUES ( '
.$Goal->ID.', '.$Hit->ID.', '.$DB->quote( 'item_ID='.$this->ID ).' )',
           
'Record goal hit of item #'.$this->ID );
    }


   
/**
     * Get link to edit post type
     *
     * @param string What attibute to return:
     *                    'link' - html tag <a>
     *                    'url' - URL
     *                    'onclick' - javascript event onclick
     * @param string Link text
     * @param string Link title
     * @return string
     */
   
function get_type_edit_link( $attr = 'link', $link_text = '', $link_title = '' )
    {
        global
$admin_url;

       
// Check if current user can edit the type of this item
       
$has_perm_edit = check_user_perm( 'item_post!CURSTATUS', 'edit', false, $this );

        if(
$has_perm_edit )
        {
// Initialize url params only when current user has a permission to edit this
           
if( $attr != 'onclick' )
            {
// Init an url
               
if( $this->ID > 0 )
                {    
// URL when item is editing:
                   
$attr_href = $admin_url.'?ctrl=items&amp;action=edit_type&amp;post_ID='.$this->ID;
                }
                elseif(
get_param( 'p' ) > 0 )
                {    
// URL when item is duplicating:
                   
$attr_href = $admin_url.'?ctrl=items&amp;action=new_type&amp;p='.get_param( 'p' );
                }
                else
                {    
// URL when item is creating:
                   
$attr_href = $admin_url.'?ctrl=items&amp;action=new_type';
                }
            }

            if(
$attr != 'url' )
            {
// Init an event 'onclick'
               
$attr_onclick = 'return b2edit_type( \''.TS_('Do you want to save your changes before changing the Post Type?').'\','
                   
.' \''.$admin_url.'?ctrl=items&amp;blog='.$this->get_blog_ID().'\','
                   
.' \''.( $this->ID > 0 ? 'edit_type' : 'new_type' ).'\' );';
            }
        }

        switch(
$attr )
        {
            case
'link':
                if( empty(
$attr_href ) )
                {
// No perm to edit item type
                   
return $link_text;
                }
                else
                {
// Current user can edit this item
                   
return '<a href="'.$attr_href.'" onclick="'.$attr_onclick.'" title="'.$link_title.'" class="post_type_link">'.$link_text.'</a>';
                }
                break;

            case
'onclick':
                return empty(
$attr_onclick ) ? '' : $attr_onclick;
                break;

            case
'url':
                return empty(
$attr_href ) ? '' : $attr_href;
                break;
        }
    }


   
/**
     * Get custom fields of post type
     *
     * @param string Type(s) of custom field: 'all', 'varchar', 'double', 'text', 'html', 'url', 'image', 'computed', 'separator'. Use comma separator to get several types
     * @param boolean TRUE to force use custom fields of current version instead of revision
     * @return array
     */
   
function get_type_custom_fields( $type = 'all', $force_current_fields = false )
    {
        if( !
$force_current_fields && $this->is_revision() )
        {    
// Get custom fields of current active revision:
           
if( ! isset( $this->custom_fields ) )
            {    
// Initialize an array only first time:
               
$this->custom_fields = array();
                if( ! empty(
$this->ID ) &&
                    (
$Revision = & $this->get_revision() ) )
                {    
// Get the custom fields from DB:
                   
global $DB;
                   
$SQL = new SQL( 'Get custom fields of revision #'.$Revision->iver_ID.'('.$Revision->iver_type.') for Item #'.$this->ID );
                   
$SQL->SELECT( 'ivcf_itcf_ID AS ID, itcf_ityp_ID AS ityp_ID, ivcf_itcf_label AS label, IFNULL( itcf_name, CONCAT( "!deleted_", ivcf_itcf_ID ) ) AS name, itcf_type AS type, IFNULL( itcf_order, 999999999 ) AS `order`, itcf_note AS note, ' );
                   
$SQL->SELECT_add( 'itcf_required AS required, itcf_meta AS meta, itcf_public AS public, itcf_format AS format, itcf_formula AS formula, itcf_disp_condition AS disp_condition, itcf_header_class AS header_class, itcf_cell_class AS cell_class, ' );
                   
$SQL->SELECT_add( 'itcf_link AS link, itcf_link_nofollow AS link_nofollow, itcf_link_class AS link_class, ' );
                   
$SQL->SELECT_add( 'itcf_line_highlight AS line_highlight, itcf_green_highlight AS green_highlight, itcf_red_highlight AS red_highlight, itcf_description AS description, itcf_merge AS merge' );
                   
$SQL->FROM( 'T_items__version_custom_field' );
                   
$SQL->FROM_add( 'LEFT JOIN T_items__type_custom_field ON ivcf_itcf_ID = itcf_ID' );
                   
$SQL->WHERE_and( 'ivcf_iver_ID = '.$DB->quote( $Revision->iver_ID ) );
                   
$SQL->WHERE_and( 'ivcf_iver_type = '.$DB->quote( $Revision->iver_type ) );
                   
$SQL->WHERE_and( 'ivcf_iver_itm_ID = '.$DB->quote( $Revision->iver_itm_ID ) );
                   
$SQL->ORDER_BY( '`order`, ivcf_itcf_ID' );
                   
$custom_fields = $DB->get_results( $SQL, ARRAY_A );
                    foreach(
$custom_fields as $custom_field )
                    {
                       
$this->custom_fields[ $custom_field['name'] ] = $custom_field;
                    }
                }
            }

            return
$this->custom_fields;
        }
        else
        {    
// Get custom fields of current item type:
           
if( ! $this->get_ItemType() )
            {
// Unknown post type
               
return array();
            }

            return
$this->ItemType->get_custom_fields( $type );
        }
    }


   
/**
     * Check if post type is enabled for the post collection
     *
     * @return boolean
     */
   
function is_type_enabled()
    {
       
$ityp_ID = intval( $this->get( 'ityp_ID' ) );

        if( empty(
$ityp_ID ) )
        {
            return
false;
        }

       
$item_Blog = & $this->get_Blog();

        return
$item_Blog->is_item_type_enabled( $ityp_ID );
    }


   
/**
     * Check if item allows the statuses for the comments (closed or disabled)
     *
     * @return boolean TRUE when item can has the comment status different of 'opened'
     */
   
function allow_comment_statuses()
    {
        if( !
$this->get_type_setting( 'use_comments' ) )
        {
// The comments are not allowed for this post type
           
return false;
        }

        if( !
$this->get_type_setting( 'allow_closing_comments' ) && ! $this->get_type_setting( 'allow_disabling_comments' ) )
        {
// The statuses 'closed' & 'disabled' are not allowed for comments of this post type
           
return false;
        }

       
$this->load_Blog();
        if(
$this->Blog->get_setting( 'allow_comments' ) == 'never' )
        {
// The comments are not allowed by Blog
           
return false;
        }

        return
true;
    }


   
/**
     * Country is visible for defining
     *
     * @return boolean TRUE if users can define a country for posts of current blog
     */
   
function country_visible()
    {
        return
$this->get_type_setting( 'use_country' ) != 'never' || $this->region_visible();
    }


   
/**
     * Region is visible for defining
     *
     * @return boolean TRUE if users can define a region for this post
     */
   
function region_visible()
    {
        return
$this->get_type_setting( 'use_region' ) != 'never' || $this->subregion_visible();
    }


   
/**
     * Subregion is visible for defining
     *
     * @return boolean TRUE if users can define a subregion for this post
     */
   
function subregion_visible()
    {
        return
$this->get_type_setting( 'use_sub_region' ) != 'never' || $this->city_visible();
    }


   
/**
     * City is visible for defining
     *
     * @return boolean TRUE if users can define a city for this post
     */
   
function city_visible()
    {
        return
$this->get_type_setting( 'use_city' ) != 'never';
    }


   
/**
     * Get the parent Item
     *
     * @return object Item
     */
   
function & get_parent_Item()
    {
        if( ! empty(
$this->parent_Item ) &&
           
$this->parent_Item->ID == $this->parent_ID )
        {    
// Return the initialized parent Item and if it is really parent of this Item:
           
return $this->parent_Item;
        }

        if( empty(
$this->parent_ID ) )
        {    
// No defined parent Item
           
$this->parent_Item = NULL;
            return
$this->parent_Item;
        }

        if(
$this->get_type_setting( 'use_parent' ) == 'never' )
        {    
// Parent Item is not allowed for current item type
           
$this->parent_Item = NULL;
            return
$this->parent_Item;
        }

       
$ItemCache = & get_ItemCache();
       
$this->parent_Item = & $ItemCache->get_by_ID( $this->parent_ID, false, false );

        return
$this->parent_Item;
    }


   
/**
     * Extract all possible tags from item contents
     *
     * @return array Tags
     */
   
function search_tags_by_content()
    {
        global
$DB;

       
// Concatenate all text item fields:
       
$search_string = $this->get( 'title' ).' '
           
.$this->get( 'content' ).' '
           
.$this->get( 'excerpt').' '
           
.$this->get( 'titletag' ).' '
           
.$this->get_setting( 'metadesc' ).' '
           
.$this->get_setting( 'metakeywords' ).' ';
       
// + all text custom fields:
       
$text_custom_fields = $this->get_type_custom_fields( 'varchar,text,html' );
        foreach(
$text_custom_fields as $field_name => $text_custom_field )
        {
           
$search_string .= $this->get_custom_field_value( $field_name ).' ';
        }

       
// Clear spaces:
       
$search_string = utf8_trim( $search_string );

        if( empty(
$search_string ) )
        {    
// This item has no content, so don't try to run a searching:
           
return array();
        }

       
// Get all possible tags that are not related to this item:
       
$other_tags_SQL = new SQL( 'Get all possible tags that are not related to this item' );
       
$other_tags_SQL->SELECT( 'tag_name' );
       
$other_tags_SQL->FROM( 'T_items__tag' );
       
// Get all current tags to exclude from searching:
       
$item_tags = $this->get_tags();
        if(
count( $item_tags ) )
        {    
// If this item has at least one tag, Exclude them:
           
$other_tags_SQL->WHERE( 'tag_name NOT IN ( '.$DB->quote( $item_tags ).' )' );
        }
       
$other_tags = $DB->get_col( $other_tags_SQL );

        if(
count( $other_tags ) == 0 )
        {    
// No tags for searching, Exit here:
           
return array();
        }

       
// Try to find each tag in content as separate word:
       
foreach( $other_tags as $i => $other_tag )
        {
            if( !
preg_match( '/\b'.$other_tag.'\b/i', $search_string ) )
            {    
// This tag is not found, Exclude it:
               
unset( $other_tags[ $i ] );
            }
        }

        return
$other_tags;
    }


   
/**
     * Restrict Item status by Collection access restriction AND by CURRENT USER write perm
     *
     * @param boolean TRUE to update status
     * @param boolean TRUE to display messages
     */
   
function restrict_status( $update_status = false, $display_messages = true )
    {
       
$item_Blog = & $this->get_Blog();

       
// Store current status to display a warning:
       
$current_status = $this->get( 'status' );

       
// Checks if the requested item status can be used by current user and if not, get max allowed item status of the collection
       
$restricted_status = $item_Blog->get_allowed_item_status( $current_status, $this );

        if(
$update_status )
        {    
// Update status to new restricted value:
           
$this->set( 'status', $restricted_status );
        }
        else
        {    
// Only change status to update it on the edit forms:
           
$this->status = $restricted_status;
        }

        if(
$current_status != $this->get( 'status' ) && $display_messages )
        {    
// If current item status cannot be used for item collection
           
global $Messages;

           
$visibility_statuses = get_visibility_statuses();
            if(
$item_Blog->get_setting( 'allow_access' ) == 'members' )
            {    
// The collection is restricted for members or only for owner
               
if( ! $item_Blog->get( 'advanced_perms' ) )
                {    
// If advanced permissions are NOT enabled then only owner has an access for the collection
                   
$Messages->add( sprintf( T_('Since this collection is "Private", the visibility of this post will be restricted to "%s".'), $visibility_statuses[ $this->status ] ), 'warning' );
                }
                else
                {    
// Otherwise all members of this collection have an access for the collection
                   
$Messages->add( sprintf( T_('Since this collection is "Members only", the visibility of this post will be restricted to "%s".'), $visibility_statuses[ $this->status ] ), 'warning' );
                }
            }
            elseif(
$item_Blog->get_setting( 'allow_access' ) == 'users' )
            {    
// The collection is restricted for logged-in users only:
               
$Messages->add( sprintf( T_('Since this collection is "Community only", the visibility of this post will be restricted to "%s".'), $visibility_statuses[ $this->status ] ), 'warning' );
            }
        }
    }


   
/**
     * Check what were already notified on this item
     *
     * @param array|string Flags, possible values: 'moderators_notified', 'members_notified', 'community_notified', 'pings_sent'
     */
   
function check_notifications_flags( $flags )
    {
        if( !
is_array( $flags ) )
        {    
// Convert string to array:
           
$flags = array( $flags );
        }

       
// TRUE if all requested flags are in current item notifications flags:
       
return ( count( array_diff( $flags, $this->get( 'notifications_flags' ) ) ) == 0 );
    }

   
/**
     * Check if notifications are allowed for this Item
     * (some item types have no permanent URL and thus cannot be sent out with a permalink)
     *
     * @return boolean TRUE if allowed
     */
   
function notifications_allowed()
    {
        return (
$this->get_type_setting( 'usage' ) != 'special' );
    }


   
/**
     * Check if this item can be displayed for current user on front-office
     *
     * @return boolean
     */
   
function can_be_displayed()
    {
        if( empty(
$this->ID ) )
        {    
// Item is not created yet, so it cannot be displayed:
           
return false;
        }

       
// Check if this Item can be displayed with current status:
       
return can_be_displayed_with_status( $this->get( 'status' ), 'post', $this->get_blog_ID(), $this->creator_user_ID );
    }


   
/*
     * Check if user can flag this item
     *
     * @return boolean
     */
   
function can_flag()
    {
        if( empty(
$this->ID ) )
        {    
// Item is not created yet:
           
return false;
        }

        if( !
is_logged_in() )
        {    
// If user is NOT logged in:
           
return false;
        }

        if(
$this->get_type_setting( 'usage' ) != 'post' )
        {    
// Only "Post" items can be flagged:
           
return false;
        }

        return
true;
    }


   
/**
     * Display button to flag item
     *
     * @param array Params
     */
   
function flag( $params = array() )
    {
        echo
$this->get_flag( $params );
    }


   
/**
     * Get button to flag item
     *
     * @param array Params
     * @return string HTML of the button
     */
   
function get_flag( $params = array() )
    {
       
$params = array_merge( array(
               
'before'       => '',
               
'after'        => '',
               
'title_flag'   => T_('You have flagged this. Click to remove flag.'),
               
'title_unflag' => T_('Click to flag this.'),
               
'only_flagged' => false, // Display the flag button only when this item is already flagged by current User
               
'allow_toggle' => true, // Allow to toggle flag state by AJAX
           
), $params );

        if( !
$this->can_flag() )
        {    
// Don't display the flag button if it is not allowed by some reason:
           
return '';
        }

       
$item_Blog = & $this->get_Blog();

       
// Get current state of flag:
       
$is_flagged = $this->get_user_data( 'item_flag' );

        if(
$params['only_flagged'] && ! $is_flagged )
        {    
// Don't display the button because of request to display it only for the flagged items by current User:
           
return '';
        }

       
$r = $params['before'];

        if(
$params['allow_toggle'] )
        {    
// Allow to toggle:
           
$r .= '<a href="#" data-id="'.$this->ID.'" data-coll="'.$item_Blog->get( 'urlname' ).'" class="action_icon evo_post_flag_btn">'
               
.get_icon( 'flag_on', 'imgtag', array(
                   
'title' => $params['title_flag'],
                   
'style' => $is_flagged ? '' : 'display:none',
                ) )
                .
get_icon( 'flag_off', 'imgtag', array(
                   
'title' => $params['title_unflag'],
                   
'style' => $is_flagged ? 'display:none' : '',
                ) )
            .
'</a>';
        }
        else
        {    
// Display only current flag state as icon:
           
$r .= '<span class="action_icon evo_post_flag_btn">'
               
.get_icon( ( $is_flagged ? 'flag_on' : 'flag_off' ), 'imgtag', array(
                       
'title' => ( $is_flagged ? $params['title_flag'] : $params['title_unflag'] ),
                    ) )
                .
'</span>';
        }

       
$r .= $params['after'];

        return
$r;
    }


   
/**
     * Flag or unflag item for current user
     *
     * @param string Vote value (positive, neutral, negative)
     * @access protected
     */
   
function update_flag()
    {
        global
$DB, $current_User, $servertimenow;

        if( !
$this->can_flag() )
        {    
// Don't display the flag button if it is not allowed by some reason:
           
return;
        }

       
$DB->begin();

       
// Get current state of flag:
       
$is_flagged = $this->get_user_data( 'item_flag' );

       
$new_flag_value = ( $is_flagged ? 0 : 1 );

        if(
is_null( $is_flagged ) )
        {    
// Flag item for current user:
           
$DB->query( 'REPLACE INTO T_items__user_data
                       ( itud_user_ID, itud_item_ID, itud_flagged_item )
                VALUES ( '
.$DB->quote( $current_User->ID ).', '.$DB->quote( $this->ID ).', '.$new_flag_value.' )',
               
'Insert user item data row to flag item #'.$this->ID );
        }
        else
        {    
// Update flag of this item for current user:
           
$DB->query( 'UPDATE T_items__user_data
                  SET itud_flagged_item = '
.$new_flag_value.'
                WHERE itud_user_ID = '
.$DB->quote( $current_User->ID ).'
                  AND itud_item_ID = '
.$DB->quote( $this->ID ),
               
'Update user item data row to flag item #'.$this->ID );
        }

       
$this->set_user_data( 'item_flag', $new_flag_value );

       
// Invalidate key for the Item data per current User:
       
BlockCache::invalidate_key( 'item_user_flag_'.$this->ID, $current_User->ID );

       
$DB->commit();
    }


   
/**
     * Check if user can vote on this item
     *
     * @return boolean
     */
   
function can_vote()
    {
        if( empty(
$this->ID ) )
        {    
// Item is not created yet:
           
return false;
        }

        if( !
is_logged_in( false ) )
        {    
// If user is NOT logged in:
           
return false;
        }

       
$item_Blog = & $this->get_Blog();

        if( empty(
$item_Blog ) || ! $item_Blog->get_setting( 'voting_positive' ) )
        {    
// If current collection doesn't allow a voting on items:
           
return false;
        }

        return
true;
    }


   
/**
     * Display buttons to vote on item if user is logged
     *
     * @param array Params
     */
   
function display_voting_panel( $params = array() )
    {
        global
$current_User;

       
$params = array_merge( array(
               
'before'                 => '',
               
'after'                  => '',
               
'class'                  => '',
               
'widget_ID'              => 0,
               
'skin_ID'                => 0,
               
'label_text'             => T_('My vote:'),
               
'title_like'             => T_('Cast a positive vote!'),
               
'title_like_voted'       => T_('You sent a positive vote.'),
               
'title_noopinion'        => T_('Cast a neutral vote!'),
               
'title_noopinion_voted'  => T_('You sent a neutral vote.'),
               
'title_dontlike'         => T_('Cast a negative vote!'),
               
'title_dontlike_voted'   => T_('You sent a negative vote.'),
               
'title_empty'            => T_('No user votes yet.'),
               
'title_own'              => T_('You cannot vote on own Item.'),
               
'display_summary'        => 'replace', // 'no' - Don't display, 'replace' - Replace label after vote, 'always' - Always display after icons
               
'display_summary_author' => true, // Display summary for author
               
'display_wrapper'        => true, // Use FALSE when you update this from AJAX request
               
'display_score'          => false,
               
'display_noactive'       => false, // Display not active icons, when current User is owner of this Item
               
'display_like'           => true,
               
'display_noopinion'      => true,
               
'display_dontlike'       => true,
               
'icon_like_active'       => 'thumb_up',
               
'icon_like_noactive'     => 'thumb_up_disabled',
               
'icon_noopinion_active'  => 'ban',
               
'icon_noopinion_noactive'=> 'ban_disabled',
               
'icon_dontlike_active'   => 'thumb_down',
               
'icon_dontlike_noactive' => 'thumb_down_disabled',
            ),
$params );

        if( !
$this->can_vote() )
        {    
// Don't display the voting panel if a voting on this item is not allowed by some reason:
           
return;
        }

        echo
$params['before'];

        if(
$params['display_wrapper'] )
        {    
// Display wrapper:
           
echo '<span id="vote_item_'.$this->ID.'" class="evo_voting_panel '.( empty( $params['class'] ) ? '' : ' '.$params['class'] ).'">';
        }

        if(
$current_User->ID == $this->creator_user_ID )
        {    
// Display only vote summary/score for users on their own items:
           
if( $params['display_noactive'] && $params['display_like'])
            {    
// Display disabled 'Like' icon:
               
echo get_icon( $params['icon_like_noactive'], 'imgtag', array( 'title' => $params['title_own'] ) );
            }

            if(
$params['display_noactive'] && $params['display_noopinion'] )
            {    
// Display disabled 'No opinion' icon:
               
echo get_icon( $params['icon_noopinion_noactive'], 'imgtag', array( 'title' => $params['title_own'] ) );
            }

            if(
$params['display_score'] )
            {    
// Display score:
               
echo '<span class="vote_score">'.$this->get( 'addvotes' ).'</span>';
            }
            elseif(
$params['display_summary_author'] )
            {    
// Display summary:
               
$params['result_title_undecided'] = T_('Voting:');
               
$params['after_result'] = '.';
               
$result_summary = $this->get_vote_summary( $params );
                echo ( !empty(
$result_summary ) ? $result_summary : $params['title_empty'] );
            }

            if(
$params['display_noactive'] && $params['display_dontlike'] )
            {    
// Display disabled 'Don't like' icon:
               
echo get_icon( $params['icon_dontlike_noactive'], 'imgtag', array( 'title' => $params['title_own'] ) );
            }
        }
        else
        {    
// Display form to vote:
           
$title_text = $params['label_text'];
           
$after_voting_form = '';

            if(
$params['display_summary'] != 'no' )
            {    
// If we should display summary:
               
$vote_result = $this->get_vote_disabled();

                if(
$vote_result['is_voted'] && $params['display_summary'] == 'replace' )
                {    
// Replace title with vote summary if user already voted on this item:
                   
$title_text = $this->get_vote_summary( $params );
                }

                if(
$params['display_summary'] == 'always' )
                {    
// Always display vote summary after icons:
                   
$after_voting_form = $this->get_vote_summary( $params );
                }
            }

           
$item_Blog = & $this->get_Blog();

           
display_voting_form( array_merge( array(
                   
'vote_type'             => 'item',
                   
'vote_ID'               => $this->ID,
                   
'display_like'          => $item_Blog->get_setting( 'voting_positive' ),
                   
'display_noopinion'     => $item_Blog->get_setting( 'voting_neutral' ),
                   
'display_dontlike'      => $item_Blog->get_setting( 'voting_negative' ),
                   
'display_inappropriate' => false,
                   
'display_spam'          => false,
                   
'title_text'            => $title_text.' ',
                ),
$params ) );

            echo
$after_voting_form;
        }

        if(
$params['display_wrapper'] )
        {    
// Display wrapper:
           
echo '</span>';
        }

        echo
$params['after'];
    }


   
/**
     * Set the vote, as a number.
     *
     * @param string Vote value (positive, neutral, negative)
     * @access protected
     */
   
function set_vote( $vote_value )
    {
        global
$DB, $current_User, $servertimenow;

        if( !
$this->can_vote() )
        {    
// A voting on this item is not allowed by some reason:
           
return;
        }

        switch (
$vote_value )
        {    
// Set a value for voting:
           
case 'positive':
               
$vote = '1';
                break;
            case
'neutral':
               
$vote = '0';
                break;
            case
'negative':
               
$vote = '-1';
                break;
            default:
               
// $vote_value is not correct from ajax request
               
return;
        }

       
$DB->begin();

       
$SQL = new SQL( 'Check if current user already voted on item #'.$this->ID );
       
$SQL->SELECT( 'itvt_updown' );
       
$SQL->FROM( 'T_items__votes' );
       
$SQL->WHERE( 'itvt_item_ID = '.$DB->quote( $this->ID ) );
       
$SQL->WHERE_and( 'itvt_user_ID = '.$DB->quote( $current_User->ID ) );
       
$existing_vote = $DB->get_var( $SQL );

        if(
$existing_vote === NULL )
        {    
// Add a new vote for first time:
            // Use a replace into to avoid duplicate key conflict in case when user clicks two times fast one after the other:
           
$DB->query( 'REPLACE INTO T_items__votes
                       ( itvt_item_ID, itvt_user_ID, itvt_updown, itvt_ts )
                VALUES ( '
.$DB->quote( $this->ID ).', '.$DB->quote( $current_User->ID ).', '.$DB->quote( $vote ).', '.$DB->quote( date2mysql( $servertimenow ) ).' )',
               
'Add new vote on item #'.$this->ID );
        }
        else
        {    
// Update a vote:
           
if( $existing_vote == $vote )
            {    
// Undo previous vote:
               
$DB->query( 'DELETE FROM T_items__votes
                    WHERE itvt_item_ID = '
.$DB->quote( $this->ID ).'
                        AND itvt_user_ID = '
.$DB->quote( $current_User->ID ),
                   
'Undo previous vote on item #'.$this->ID );
            }
            else
            {    
// Set new vote:
               
$DB->query( 'UPDATE T_items__votes
                        SET itvt_updown = '
.$DB->quote( $vote ).'
                    WHERE itvt_item_ID = '
.$DB->quote( $this->ID ).'
                        AND itvt_user_ID = '
.$DB->quote( $current_User->ID ),
                   
'Update a vote on item #'.$this->ID );
            }
        }

       
$vote_SQL = new SQL( 'Get voting results of item #'.$this->ID );
       
$vote_SQL->SELECT( 'COUNT( itvt_updown ) AS votes_count, SUM( itvt_updown ) AS votes_sum' );
       
$vote_SQL->FROM( 'T_items__votes' );
       
$vote_SQL->WHERE( 'itvt_item_ID = '.$DB->quote( $this->ID ) );
       
$vote_SQL->WHERE_and( 'itvt_updown IS NOT NULL' );
       
$vote = $DB->get_row( $vote_SQL );

       
// These values must be number and not NULL:
       
$vote->votes_sum = intval( $vote->votes_sum );
       
$vote->votes_count = intval( $vote->votes_count );

       
// Update fields with vote counters for this item:
       
$DB->query( 'UPDATE T_items__item
              SET post_addvotes = '
.$DB->quote( $vote->votes_sum ).',
                  post_countvotes = '
.$DB->quote( $vote->votes_count ).'
            WHERE post_ID = '
.$DB->quote( $this->ID ),
           
'Update fields with vote counters for item #'.$this->ID );
       
$this->addvotes = $vote->votes_sum;
       
$this->countvotes = $vote->votes_count;

       
$DB->commit();

        return;
    }


   
/**
     * Get the vote helpful type disabled, as array.
     *
     * @return array Result:
     *               'is_voted' - TRUE if current user already voted on this comment
     *               'icons_statuses': array( 'yes', 'no' )
     */
   
function get_vote_disabled()
    {
        global
$DB, $current_User;

       
$result = array(
               
'is_voted' => false,
               
'icons_statuses' => array(
                   
'yes' => '',
                   
'no' => ''
           
) );

        if( !
$this->can_vote() )
        {    
// A voting on this item is not allowed by some reason:
           
$result;
        }

       
$SQL = new SQL( 'Get a vote result for current for item #'.$this->ID );
       
$SQL->SELECT( 'itvt_updown' );
       
$SQL->FROM( 'T_items__votes' );
       
$SQL->WHERE( 'itvt_item_ID = '.$DB->quote( $this->ID ) );
       
$SQL->WHERE_and( 'itvt_user_ID = '.$DB->quote( $current_User->ID ) );
       
$SQL->WHERE_and( 'itvt_updown IS NOT NULL' );

        if(
$vote = $DB->get_row( $SQL ) )
        {    
// Get a vote for current user and this item:
           
$result['is_voted'] = true;
           
$class_disabled = 'disabled';
           
$class_voted = 'voted';
            switch (
$vote->itvt_updown )
            {
                case
'1': //
                   
$result['icons_statuses']['yes'] = $class_voted;
                   
$result['icons_statuses']['no'] = $class_disabled;
                    break;
                case
'-1': // NO
                   
$result['icons_statuses']['no'] = $class_voted;
                   
$result['icons_statuses']['yes'] = $class_disabled;
                    break;
            }
        }

        return
$result;
    }


   
/**
     * Get the vote summary, as a string.
     *
     * @param type Vote type (spam, helpful)
     * @param srray Params
     * @return string
     */
   
function get_vote_summary( $params = array() )
    {
       
$params = array_merge( array(
               
'result_title'           => '',
               
'result_title_undecided' => '',
               
'after_result'           => '',
            ),
$params );

        if( !
$this->can_vote() )
        {    
// A voting on this item is not allowed by some reason:
           
return '';
        }

        if(
$this->countvotes == 0 )
        {    
// No votes for current comment:
           
return '';
        }

       
$item_Blog = & $this->get_Blog();

        if(
$item_Blog->get_setting( 'voting_positive' ) &&
            !
$item_Blog->get_setting( 'voting_neutral' ) &&
            !
$item_Blog->get_setting( 'voting_negative' ) )
        {    
// Only the likes are enabled, Display a count of them:
           
$summary = ( $this->countvotes > 0 ) ? sprintf( T_('%s Likes'), $this->countvotes ) : T_('No likes');
        }
        else
        {    
// Calculate vote summary in percents:
           
$summary = ceil( $this->addvotes / $this->countvotes * 100 );

            if(
$summary < -20 )
            {    
// Item is positive
               
$summary = abs( $summary ).'% '.T_('Negative');
            }
            else if(
$summary >= -20 && $summary <= 20 )
            {    
// Item is UNDECIDED:
               
$summary = T_('UNDECIDED');
                if( !empty(
$params['result_title_undecided'] ) )
                {    
// Display title before undecided results:
                   
$summary = $params['result_title_undecided'].' '.$summary;
                }
            }
            else if(
$summary > 20 )
            {    
// Item is negative:
               
$summary .= '% '.T_('Positive');
            }
        }

        if( !empty(
$params['result_title'] ) )
        {    
// Display title before results:
           
$summary = $params['result_title'].' '.$summary;
        }

        return
$summary.$params['after_result'].' ';
    }


   
/**
     * Get a message to display before comment form
     *
     * @return string
     */
   
function get_comment_form_msg()
    {
        if(
$this->get_type_setting( 'allow_comment_form_msg' ) )
        {    
// If custom message is allowed by Item Type:
           
$item_msg = trim( $this->get_setting( 'comment_form_msg' ) );
            if( ! empty(
$item_msg ) )
            {    
// Use custom message of this item:
               
return $item_msg;
            }
        }

       
// Try to use a message from Collection setting:
       
$item_Blog = & $this->get_Blog();
       
$collection_msg = trim( $item_Blog->get_setting( 'comment_form_msg' ) );
        if( ! empty(
$collection_msg ) )
        {    
// Use a message of the item type:
           
return $collection_msg;
        }

       
// Try to use a message from Item Type setting:
       
$item_type_msg = trim( $this->get_type_setting( 'comment_form_msg' ) );
        if( ! empty(
$item_type_msg ) )
        {    
// Use a message of the item type:
           
return $item_type_msg;
        }
    }


   
/**
     * Display a message before comment form
     *
     * @param array Params
     */
   
function display_comment_form_msg( $params = array() )
    {
       
$params = array_merge( array(
               
'before' => '<div class="alert alert-warning">',
               
'after'  => '</div>',
            ),
$params );

       
// Get a message:
       
$comment_form_msg = $this->get_comment_form_msg();

        if( empty(
$comment_form_msg ) )
        {    
// No message to display before comment form, Exit here:
           
return;
        }

       
// Display a message:
       
echo $params['before'];
        echo
nl2br( $comment_form_msg );
        echo
$params['after'];
    }


   
/**
     * Check if current User has a permission to refresh a contents last updated date of this Item
     *
     * @return boolean
     */
   
function can_refresh_contents_last_updated()
    {
        if( !
$this->ID )
        {    
// If this Item is not saved in DB yet:
           
return false;
        }

        if( !
check_user_perm( 'item_post!CURSTATUS', 'edit', false, $this, false ) )
        {    
// If user has no perm to edit this Item:
           
return false;
        }

       
// No restriction, Current User has a permission to refresh a contents last updated date of this Item:
       
return true;
    }


   
/*
     * Get URL to refresh a contents last updated date of this Item if user has refresh rights
     *
     * @param array Params
     * @return string|boolean URL or FALSE if current user has no perm
     */
   
function get_refresh_contents_last_updated_url( $params = array() )
    {
        if( !
$this->can_refresh_contents_last_updated() )
        {    
// If current User has no perm to refresh:
           
return false;
        }

       
$params = array_merge( array(
               
'glue' => '&amp;',
               
'type' => 'touch', // What comment date use to update: 'touch' - 'comment_last_touched_ts', 'create' - 'comment_date'
           
), $params );

       
$url = get_htsrv_url().'action.php?mname=collections'.$params['glue']
            .
'action=refresh_contents_last_updated'.$params['glue']
            .
'item_ID='.$this->ID.$params['glue']
            .(
$params['type'] != 'touch' ? 'type='.$params['type'].$params['glue'] : '' )
            .
url_crumb( 'collections_refresh_contents_last_updated' );

        return
$url;
    }


   
/**
     * Get a link to refresh a contents last updated date of this Item if user has refresh rights
     *
     * @param array Params
     */
   
function get_refresh_contents_last_updated_link( $params = array() )
    {
       
$params = array_merge( array(
               
'before' => ' ',
               
'after'  => '',
               
'text'   => '#icon#',
               
'title'  => '#',
               
'class'  => '',
            ),
$params );

       
$refresh_url = $this->get_refresh_contents_last_updated_url( $params );
        if( !
$refresh_url )
        {    
// If current user has no perm to refesh contents last updated date of this Item:
           
return;
        }

        if(
$params['title'] == '#' )
        {    
// Use default title
           
$params['title'] = T_('Reset the "contents last updated" date to the latest content change on this thread');
        }

       
$params['text'] = utf8_trim( $params['text'] );
       
$params['title'] = utf8_trim( $params['title'] );
       
$params['class'] = utf8_trim( $params['class'] );

       
$r = $params['before'];

       
$r .= '<a href="'.$refresh_url.'"'
               
.( empty( $params['title'] ) ? '' : ' title="'.format_to_output( $params['title'], 'htmlattr' ).'"' )
                .( empty(
$params['class'] ) ? '' : ' class="'.$params['class'].'"' )
            .
'>'
               
.str_replace( '#icon#', get_icon( 'refresh', 'imgtag', array( 'title' => $params['title'] ) ), $params['text'] )
            .
'</a>';

       
$r .= $params['after'];

        return
$r;
    }


   
/**
     * Refresh contents last updated ts with date of the latest Comment
     *
     * @param boolean TRUE to display messages
     * @param string Field name(without prefix "comment_") of the latest comment which should be used to refresh the post date column: 'date', 'last_touched_ts'
     * @param string What post and comment date fields use to refresh:
                       'touched' - 'post_datemodified', 'comment_last_touched_ts' (Default)
                       'created' - 'post_datestart', 'comment_date'
     * @return boolean TRUE of success
     */
   
function refresh_contents_last_updated_ts( $display_messages = false, $date_type = 'touched' )
    {
        if( !
$this->can_refresh_contents_last_updated() )
        {    
// If current User has no permission to refresh a contents last updated date of the requested Item:
           
return false;
        }

        global
$DB, $Messages;

       
// Clear latest Comment from previous calling before Comment updating:
       
$this->latest_Comment = NULL;

        if(
$date_type == 'created' )
        {    
// Use dates for 'created' mode:
           
$post_date_field = 'datestart';
           
$comment_date_field = 'date';
        }
        else
        {    
// Use dates for 'touched' mode:
           
$post_date_field = 'datemodified';
           
$comment_date_field = 'last_touched_ts';
        }

        if(
$latest_Comment = & $this->get_latest_Comment( get_inskin_statuses( $this->get_blog_ID(), 'comment' ), $comment_date_field ) )
        {    
// Use date from the latest public Comment:
           
$new_contents_last_updated_ts = $latest_Comment->get( $comment_date_field );
            if(
$display_messages )
            {    
// Display message:
               
$Messages->add( sprintf(
                        (
$date_type == 'created'
                           
? T_('"Contents last updated" timestamp has been refreshed using <a %s>most recently added comment</a> date = %s.')
                            :
T_('"Contents last updated" timestamp has been refreshed using <a %s>most recently touched comment</a> date = %s.')
                        ),
                       
'href="'.$latest_Comment->get_permanent_url().'"',
                       
mysql2localedatetime( $new_contents_last_updated_ts )
                    ),
'success' );
            }
        }
        else
        {    
// Use date from issue date of this Item when it has no comments yet:
           
$new_contents_last_updated_ts = $this->get( $post_date_field );
            if(
$display_messages )
            {    
// Display message:
               
$Messages->add( sprintf(
                        (
$date_type == 'created'
                           
? T_('"Contents last updated" timestamp has been refreshed using post issue date = %s.')
                            :
T_('"Contents last updated" timestamp has been refreshed using post modified date = %s.')
                        ),
                       
mysql2localedatetime( $new_contents_last_updated_ts )
                    ),
'success' );
            }
        }

       
$DB->query( 'UPDATE T_items__item
                    SET post_contents_last_updated_ts = '
.$DB->quote( $new_contents_last_updated_ts ).'
                WHERE post_ID = '
.$this->ID );

        return
true;
    }


   
/**
     * Get image file used for social media
     *
     * @param boolean Use category social media boiler plate or category image as fallback
     * @param boolean Use site social media boiler plate or site logo as fallback
     * @return object Image File or Link object
     */
   
function get_social_media_image( $use_category_fallback = false, $use_site_fallback = false, $return_as_link = false )
    {
        if( ! empty(
$this->social_media_image_File ) && ! $return_as_link )
        {
            return
$this->social_media_image_File;
        }

       
$LinkOwner = new LinkItem( $this );
        if(  
$LinkList = $LinkOwner->get_attachment_LinkList( 1000, 'cover,background,teaser,teaserperm,teaserlink,inline', 'image', array(
               
'sql_select_add' => ', CASE WHEN link_position = "cover" THEN 1 WHEN link_position IN ( "teaser", "teaserperm", "teaserlink" ) THEN 2 ELSE 3 END AS link_priority',
               
'sql_order_by'   => 'link_priority ASC, link_order ASC' ) ) )
        {
// Item has linked files
           
while( $Link = & $LinkList->get_next() )
            {
                if( ! (
$File = & $Link->get_File() ) )
                {
// No File object
                   
global $Debuglog;
                   
$Debuglog->add( sprintf( 'Link ID#%d of item #%d does not have a file object!', $Link->ID, $this->ID ), array( 'error', 'files' ) );
                    continue;
                }

                if( !
$File->exists() )
                {
// File doesn't exist
                   
global $Debuglog;
                   
$Debuglog->add( sprintf( 'File linked to item #%d does not exist (%s)!', $this->ID, $File->get_full_path() ), array( 'error', 'files' ) );
                    continue;
                }

                if(
$File->is_image() )
                {
// Use only image files for og:image tag
                   
$this->social_media_image_File = $File;
                    if(
$return_as_link )
                    {
                        return
$Link;
                    }
                    break;
                }
            }
        }

        if( empty(
$this->social_media_image_File ) && $use_category_fallback )
        {
           
$FileCache = & get_FileCache();
            if(
$default_Chapter = & $this->get_main_Chapter() )
            {
// Try social media boilerplate image
               
$social_media_image_file_ID = $default_Chapter->get( 'social_media_image_file_ID', false );
                if(
$social_media_image_file_ID > 0 && ( $File = & $FileCache->get_by_ID( $social_media_image_file_ID  ) ) && $File->is_image() )
                {
                   
$this->social_media_image_File = $File;
                }
                else
                {
// Try category image
                   
$cat_image_file_ID = $default_Chapter->get( 'image_file_ID', false );
                    if(
$cat_image_file_ID > 0 && ( $File = & $FileCache->get_by_ID( $cat_image_file_ID ) ) && $File->is_image() )
                    {
                       
$this->social_media_image_File = $File;
                    }
                }
            }
        }

        if( empty(
$this->social_media_image_File ) && $use_site_fallback )
        {
// Use social media boilerplate logo if configured
           
global $Settings;

           
$FileCache = & get_FileCache();
           
$social_media_image_file_ID = intval( $Settings->get( 'social_media_image_file_ID' ) );
            if(
$social_media_image_file_ID > 0 && ( $File = $FileCache->get_by_ID( $social_media_image_file_ID, false ) ) && $File->is_image() )
            {
               
$this->social_media_image_File = $File;
            }
            else
            {
// Use site logo as fallback if configured
               
$notification_logo_file_ID = intval( $Settings->get( 'notification_logo_file_ID' ) );
                if(
$notification_logo_file_ID > 0 && ( $File = $FileCache->get_by_ID( $notification_logo_file_ID, false ) ) && $File->is_image() )
                {
                   
$this->social_media_image_File = $File;
                }
            }
        }

        return
$this->social_media_image_File;
    }


   
/**
     * Add tags to current User
     */
   
function tag_user()
    {
        if( empty(
$this->ID ) )
        {    
// Item is not saved in DB
           
return;
        }

        if( !
is_logged_in() )
        {    
// User is not logged in
           
return;
        }

       
$item_user_tags = trim( $this->get_setting( 'user_tags' ), ' ,' );
        if( empty(
$item_user_tags ) )
        {    
// This Item has no tags for users:
           
return;
        }

        global
$current_User;

       
// Add tags to current User:
       
$current_User->add_usertags( $item_user_tags );
       
$current_User->dbupdate();
    }


   
/**
     * Get ID of next version
     *
     * @param string Version type: 'archived', 'proposed'
     * @param integer
     */
   
function get_next_version_ID( $type = 'archived' )
    {
        global
$DB;

       
// Get next version ID:
       
$iver_SQL = new SQL();
       
$iver_SQL->SELECT( 'MAX( iver_ID )' );
       
$iver_SQL->FROM( 'T_items__version' );
       
$iver_SQL->WHERE( 'iver_itm_ID = '.$this->ID );
       
$iver_SQL->WHERE_and( 'iver_type = '.$DB->quote( $type ) );

        return
intval( $DB->get_var( $iver_SQL->get() ) ) + 1;
    }


   
/**
     * Create a new item revision
     *
     * @return integer/boolean ID of created item revision if successful, otherwise False
     */
   
function create_revision()
    {
        global
$DB;

        if( empty(
$this->ID ) )
        {    
// Don't try to create revision when Item is not created yet:
           
return false;
        }

       
// Get next version ID:
       
$iver_ID = $this->get_next_version_ID( 'archived' );

       
$DB->begin( 'SERIALIZABLE' );

       
$result = $DB->query( 'INSERT INTO T_items__version
                ( iver_ID, iver_itm_ID, iver_edit_user_ID, iver_edit_last_touched_ts, iver_status, iver_title, iver_content )
                SELECT '
.$iver_ID.' AS iver_ID, post_ID, post_lastedit_user_ID, post_last_touched_ts, post_status, post_title, post_content
                  FROM T_items__item
                  WHERE post_ID = '
.$this->ID,
           
'Save a version of the Item' ) !== false;

        if(
$result )
        {    
// Create a revision for custom fields:
           
$custom_fields = $this->get_type_custom_fields();
            if(
count( $custom_fields ) )
            {    
// If at least one custom field has been updated:
               
$custom_field_values = array();
                foreach(
$custom_fields as $custom_field_name => $custom_field )
                {
                    if( isset(
$this->dbchanges_custom_fields[ $custom_field_name ] ) )
                    {
                       
$custom_field_values[] = '('.$iver_ID.','.$DB->quote( $this->ID ).','.$custom_field['ID'].','.$DB->quote( $custom_field['label'] ).','.$DB->quote( $this->dbchanges_custom_fields[ $custom_field_name ] ).')';
                    }
                }
                if(
count( $custom_field_values ) )
                {
                   
$result = $DB->query( 'INSERT INTO T_items__version_custom_field
                            ( ivcf_iver_ID, ivcf_iver_itm_ID, ivcf_itcf_ID, ivcf_itcf_label, ivcf_value )
                            VALUES '
.implode( ',', $custom_field_values ),
                       
'Save a version of the custom fields' ) !== false;
                }
            }
        }

        if(
$result )
        {    
// Create a revision for links/attachments:
           
$LinkOwner = new LinkItem( $this );
           
$existing_Links = & $LinkOwner->get_Links();

            if( ! empty(
$existing_Links ) )
            {
               
$link_values = array();
                foreach(
$existing_Links as $loop_Link )
                {
                   
$link_values[] = '('.$iver_ID.','.$this->ID.','.$loop_Link->ID.','.$loop_Link->file_ID.','.$DB->quote( $loop_Link->position ).','.$loop_Link->order.')';
                }
               
$result = $DB->query( 'INSERT INTO T_items__version_link
                        ( ivl_iver_ID, ivl_iver_itm_ID, ivl_link_ID, ivl_file_ID, ivl_position, ivl_order )
                        VALUES '
.implode( ',', $link_values ),
                   
'Save a version of attachments' ) !== false;
            }
        }

        if(
$result )
        {
           
$DB->commit();
        }
        else
        {
           
$DB->rollback();
        }

        return
$result ? $iver_ID : false;
    }


   
/**
     * Check if this Item has at least one proposed change
     *
     * @param boolean
     */
   
function has_proposed_change()
    {
        if( ! isset(
$this->has_proposed_change ) )
        {    
// Check if this Item has a proposed change and save the result in cache var:
           
if( empty( $this->ID ) )
            {    
// Item must be created to have the proposed changes:
               
return false;
            }

            global
$DB;

           
$SQL = new SQL( 'Check if Item #'.$this->ID.' has at least one proposed change' );
           
$SQL->SELECT( 'iver_ID' );
           
$SQL->FROM( 'T_items__version' );
           
$SQL->WHERE( 'iver_itm_ID = '.$this->ID );
           
$SQL->WHERE_and( 'iver_type = "proposed"' );

           
$this->has_proposed_change = (boolean) $DB->get_var( $SQL->get(), 0, NULL, $SQL->title );
        }

        return
$this->has_proposed_change;
    }


   
/**
     * Check if current user can create new proposed change
     *
     * @param boolean TRUE to redirect back if current user cannot create a proposed change
     * @return boolean
     */
   
function can_propose_change( $redirect = false )
    {
        global
$current_User, $Messages;

        if(
$redirect )
        {    
// Set a redirect URL:
           
$redirect_to = get_returnto_url();
           
$inskin_edit_script = '/item_edit.php';
            if(
strpos( $redirect_to, $inskin_edit_script ) == strlen( $redirect_to ) - strlen( $inskin_edit_script ) ||
               
strpos( $redirect_to, '?disp=proposechange' ) !== false )
            {    
// Fix a redirect page to correct in-skin editing page
               
if( ! ( $redirect_to = $this->get_edit_url( array( 'force_in_skin_editing' => true ) ) ) )
                {
                   
$redirect_to = $this->get_permanent_url( '', '', '' );
                }
            }
        }

        if( !
is_logged_in() )
        {    
// User must be logged in:
           
if( $redirect )
            {    
// Redirect back to previous page
               
header_redirect( $redirect_to );
            }
            return
false;
        }

        if( !
check_user_perm( 'blog_item_propose', 'edit', false, $this->get_blog_ID() ) )
        {    
// User has no right to propose a change for this Item:

            // Display a message:
            // NOTE: Do NOT translate this because it should not be displayed from normal UI:
           
$Messages->add( 'You don\'t have a permission to propose a change for the Item.', 'error' );

            if(
$redirect )
            {    
// Redirect back to previous page
               
header_redirect( $redirect_to );
            }
            return
false;
        }

        if( (
$last_proposed_Revision = $this->get_revision( 'last_proposed' ) ) &&
           
$last_proposed_Revision->iver_edit_user_ID != $current_User->ID )
        {    
// Don't allow to propose when previous proposition was created by another user:
           
$UserCache = & get_UserCache();
           
$User = & $UserCache->get_by_ID( $last_proposed_Revision->iver_edit_user_ID, false, false );

           
// Display a message:
           
$Messages->add( sprintf( T_('You cannot currently propose a change because previous changes by %s are pending review.'),
                (
$User ? $User->get_identity_link() : '<span class="user deleted">'.T_('Deleted user').'</span>' ) ), 'error' );

            if(
$redirect )
            {    
// Redirect back to previous page
               
header_redirect( $redirect_to );
            }
            return
false;
        }

        return
true;
    }


   
/**
     * Create a new proposed change
     *
     * @return integer/boolean ID of created item revision if successful, otherwise False
     */
   
function create_proposed_change()
    {
        global
$DB, $current_User, $Plugins_admin, $localtimenow;

        if( empty(
$this->ID ) || ! is_logged_in() )
        {    
// Item must be created and current user must be logged in:
           
return false;
        }

        if(
$this->get_type_setting( 'allow_html' ) )
        {    
// HTML is allowed for this post, we'll accept HTML tags:
           
$text_format = 'html';
        }
        else
        {    
// HTML is disallowed for this post, we'll encode all special chars:
           
$text_format = 'htmlspecialchars';
        }

       
// Never allow html content on post titles:  (fp> probably so as to not mess up backoffice and all sorts of tools)
       
param( 'post_title', 'htmlspecialchars', NULL );

       
param( 'content', $text_format, NULL );

       
// Do some optional filtering on the content
        // Typically stuff that will help the content to validate
        // Useful for code display.
        // Will probably be used for validation also.
        // + APPLY RENDERING from Rendering Plugins:
       
$Plugins_admin = & get_Plugins_admin();
       
$params = array(
               
'object_type' => 'Item',
               
'object'      => & $this,
               
'object_Blog' => & $this->Blog
           
);
       
$Plugins_admin->filter_contents( $GLOBALS['post_title'] /* by ref */, $GLOBALS['content'] /* by ref */, $this->get_renderers(), $params /* by ref */ );

       
// Title checking:
       
if( $this->get_type_setting( 'use_title' ) == 'required' )
        {
           
param_check_not_empty( 'post_title', T_('Please provide a title.'), '' );
        }

       
// Format raw HTML input to cleaned up and validated HTML:
       
param_check_html( 'content', T_('Invalid content.') );
       
$this->set( 'content', prepare_item_content( get_param( 'content' ) ) );

       
$this->set( 'title', get_param( 'post_title' ) );

        if( empty(
$iver_content ) && $this->get_type_setting( 'use_text' ) == 'required' )
        {    
// Content must be entered:
           
param_check_not_empty( 'content', T_('Please enter some text.'), '' );
        }

       
// Load values of custom fields:
       
$this->load_custom_fields_from_Request();

        if(
param_errors_detected() )
        {    
// Exit here if some errors on the submitted form:
           
return false;
        }
       
// END of loading the proposed change this this Item from request.

       
$DB->begin( 'SERIALIZABLE' );

       
// Get next version ID:
       
$iver_ID = $this->get_next_version_ID( 'proposed' );

       
$result = $DB->query( 'INSERT INTO T_items__version ( iver_ID, iver_type, iver_itm_ID, iver_edit_user_ID, iver_edit_last_touched_ts, iver_status, iver_title, iver_content )
            VALUES ( '
.$iver_ID.', '
               
.'"proposed", '
               
.$this->ID.', '
               
.$current_User->ID.', '
               
.$DB->quote( date2mysql( $localtimenow ) ).','
               
.$DB->quote( $this->get( 'status' ) ).','
               
.$DB->quote( $this->get( 'title' ) ).','
               
.$DB->quote( $this->get( 'content' ) ).' )',
           
'Save a proposed change of the Item #'.$this->ID ) !== false;

        if(
$result && ( $custom_fields = $this->get_type_custom_fields() ) )
        {    
// Save custom fields of the proposition:
           
$custom_fields_insert_sql = array();
            foreach(
$custom_fields as $custom_field )
            {
               
$custom_fields_insert_sql[] = '( '.$iver_ID.', '
                   
.'"proposed", '
                   
.$this->ID.', '
                   
.$DB->quote( $custom_field['ID'] ).','
                   
.$DB->quote( $custom_field['label'] ).','
                   
.$DB->quote( $this->get_custom_field_value( $custom_field['name'] ) ).' )';
            }
           
$result = $DB->query( 'INSERT INTO T_items__version_custom_field ( ivcf_iver_ID, ivcf_iver_type, ivcf_iver_itm_ID, ivcf_itcf_ID, ivcf_itcf_label, ivcf_value )
                VALUES '
.implode( ', ', $custom_fields_insert_sql ),
               
'Save custom fields for a proposed change of the Item #'.$this->ID ) !== false;
        }

        if(
$result )
        {    
// Save links/attachments of current version to new created proposed change:
            // TODO: This is a temporary solution. We must allow to edit links/attachements for a proposed change and store them here!
           
$result = $DB->query( 'INSERT INTO T_items__version_link
                    ( ivl_iver_ID, ivl_iver_type, ivl_iver_itm_ID, ivl_link_ID, ivl_file_ID, ivl_position, ivl_order )
                    SELECT '
.$iver_ID.', "proposed", link_itm_ID, link_ID, link_file_ID, link_position, link_order
                      FROM T_links
                     WHERE link_itm_ID = '
.$this->ID,
               
'Save custom fields for a proposed change of the Item #'.$this->ID ) !== false;
        }

        if(
$result )
        {
           
$DB->commit();
           
// Send email notification to moderators about new proposed change:
           
$this->send_proposed_change_notification( $iver_ID );
        }
        else
        {
           
$DB->rollback();
        }

        return
$result;
    }


   
/**
     * Check if this item can be updated depending on proposed changes
     *
     * @param boolean|string FALSE to don't display message of restriction, Message type: 'error', 'warning', 'note', 'success'
     * @return boolean
     */
   
function check_proposed_change_restriction( $restriction_message_type = false )
    {
        if( ! isset(
$this->check_proposed_change_restriction ) )
        {    
// Check and save result in cache var:
           
if( empty( $this->ID ) )
            {    
// Item is not created yet, so it can be updated, i.e. insert new record without restriction:
               
$this->check_proposed_change_restriction = true;
            }
            elseif(
$last_proposed_Revision = $this->get_revision( 'last_proposed' ) )
            {    
// Don't allow to edit this Item if it has at least one proposed change:
               
if( $restriction_message_type !== false )
                {    
// Display a message to inform user about this restriction:
                   
global $Messages, $admin_url;

                   
$UserCache = & get_UserCache();
                   
$User = & $UserCache->get_by_ID( $last_proposed_Revision->iver_edit_user_ID, false, false );

                   
$Messages->add( sprintf( T_('The content below includes <a %s>proposed changes</a> submitted by %s. If you edit the post, the changes will be considered accepted and you will save a new version with your own changes.'),
                           
'href="'.$admin_url.'?ctrl=items&amp;action=history&amp;p='.$this->ID.'"',
                            (
$User ? $User->get_identity_link() : '<span class="user deleted">'.T_('Deleted user').'</span>' )
                        ),
$restriction_message_type );
                }
               
$this->check_proposed_change_restriction = false;
            }
            else
            {    
// Item can be updated:
               
$this->check_proposed_change_restriction = true;
            }
        }

        return
$this->check_proposed_change_restriction;
    }


   
/**
     * Update folder of Item's attachments
     *
     * @return boolean TRUE if at least one attachment was moved to current slug folder
     */
   
function update_attachments_folder()
    {
        global
$DB, $Debuglog;

        if( empty(
$this->ID ) )
        {    
// Item must be saved in DB:
           
return false;
        }

       
$FileRootCache = & get_FileRootCache();
        if( ! (
$item_FileRoot = & $FileRootCache->get_by_type_and_ID( 'collection', $this->get_blog_ID(), true ) ) )
        {    
// Unknown file root:
           
return false;
        }

       
$LinkOwner = new LinkItem( $this );

        if( !
$LinkList = $LinkOwner->get_attachment_LinkList() )
        {    
// Item has no attachments:
           
return false;
        }

       
// Folder with current/new slug:
       
$item_new_slug_folder_name = 'quick-uploads/'.$this->get( 'urltitle' ).'/';
       
$item_new_slug_folder_path = $item_FileRoot->ads_path.$item_new_slug_folder_name;

       
$result = false;
       
$folders_of_moved_files = array();
        while(
$Link = & $LinkList->get_next() )
        {
            if( ! (
$File = & $Link->get_File() ) )
            {    
// No File object
               
$Debuglog->add( sprintf( 'Link ID#%d of item #%d does not have a file object!', $Link->ID, $this->ID ), array( 'error', 'files' ) );
                continue;
            }

            if( !
$File->exists() )
            {    
// File doesn't exist
               
$Debuglog->add( sprintf( 'File linked to item #%d does not exist (%s)!', $this->ID, $File->get_full_path() ), array( 'error', 'files' ) );
                continue;
            }

            if(
strpos( $File->get_rdfp_rel_path(), 'quick-uploads/' ) !== 0 ||
                (
strpos( $File->get_rdfp_rel_path(), $item_new_slug_folder_name ) === 0 &&
                  (
$file_FileRoot = & $File->get_FileRoot() ) &&
                 
$item_FileRoot->ID == $file_FileRoot->ID ) )
            {    
// Skip if File is not located in the folder "quick-uploads/"
                //      or File is already located in the current slug folder of this Item:
               
continue;
            }

           
$SQL = new SQL( 'Check File #'.$File->ID.' for muplitle Links before moving to slug folder of Item #'.$this->ID );
           
$SQL->SELECT( 'COUNT( link_ID )' );
           
$SQL->FROM( 'T_links' );
           
$SQL->WHERE( 'link_file_ID = '.$DB->quote( $File->ID ) );
            if(
$DB->get_var( $SQL ) > 1 )
            {    
// Don't move if File is linked with several Items:
               
continue;
            }

            if( !
file_exists( $item_new_slug_folder_path ) )
            {    
// Try to create a folder for new item slug:
               
if( ! mkdir_r( $item_new_slug_folder_path ) )
                {    
// Stop trying to move other files when no file rights to create new folder on the disc:
                   
$log_message = 'No file rights to create a folder %s before moving Item\'s files to slug folder!';
                   
$Debuglog->add( sprintf( $log_message, '"'.$item_new_slug_folder_path.'"' ), array( 'error', 'files' ) );
                   
syslog_insert( sprintf( $log_message, '[['.$item_new_slug_folder_path.']]'), 'error', 'item', $this->ID );
                    return
false;
                }
            }

           
// Save file folder before moving:
           
$old_file_dir = isset( $File->_dir ) ? $File->_dir : false;

           
// Move File to the folder with name as current Item's slug:
           
if( $File->move_to( $item_FileRoot->type, $item_FileRoot->in_type_ID, $item_new_slug_folder_name.$File->get_name(), true ) )
            {    
// If File was moved successfully
               
$result = true;
                if(
$old_file_dir && ! in_array( $old_file_dir, $folders_of_moved_files ) )
                {    
// Collect a folder in order to check and remove if it is empty after moving all files from the folder:
                   
$folders_of_moved_files[] = $old_file_dir;
                }
            }
        }

       
// Delete folders which are empty after moving all files:
       
foreach( $folders_of_moved_files as $folder_of_moved_files )
        {
            if(
file_exists( $folder_of_moved_files ) &&
               
is_empty_directory( $folder_of_moved_files ) &&
                !
rmdir_r( $folder_of_moved_files ) )
            {    
// Log error:
               
$log_message = 'No file rights to delete an empty folder %s after moving Item\'s files to slug folder!';
               
$Debuglog->add( sprintf( $log_message, '"'.$folder_of_moved_files.'"' ), array( 'error', 'files' ) );
               
syslog_insert( sprintf( $log_message, '[['.$folder_of_moved_files.']]' ), 'warning', 'item', $this->ID );
            }
        }

        return
$result;
    }


   
/**
     * Display or log notification message
     *
     * @param string Message
     * @param boolean|string 'cron_job' - to log messages for cron job, FALSE - to don't log
     * @param string Message type
     * @param string Message group title
     */
   
function display_notification_message( $message, $log_messages = false, $message_type = 'note', $message_group = NULL )
    {
        global
$Messages;

        if(
$log_messages == 'cron_job' )
        {    
// Log message for cron job:
           
cron_log_append( $message."\n", $message_type );
        }
        elseif( ! empty(
$this->ID ) && // Item must be stored in DB
            // User must be logged in and activated and be a collection admin
           
check_user_perm( 'blog_admin', 'edit', false, $this->get_blog_ID(), false ) )
        {    
// Display notification message only for collection admin:
           
if( $message_group === NULL )
            {    
// Set default group title:
               
$message_group = T_('Sending notifications:');
            }
           
$Messages->add_to_group( $message, $message_type, $message_group );
        }
    }


   
/**
     * Get available locales
     *
     * @param string Type of locales:
     *        - 'locale' - locales of the Item's collection,
     *        - 'coll' - locales as links to other collections,
     *        - 'all' - all locales of this and links with other collections.
     * @return array
     */
   
function get_available_locales( $type = 'locale' )
    {
        return (
$item_Blog = & $this->get_Blog() ? $item_Blog->get_locales( $type ) : array() );
    }


   
/**
     * Get locale options for selector on edit page
     *
     * @param string Type of locales:
     *        - 'locale' - locales of the Item's collection,
     *        - 'coll' - locales as links to other collections,
     *        - 'all' - all locales of this and links with other collections.
     * @param boolean Exclude locales that are already used in the group of this Item
     * @return string
     */
   
function get_locale_options( $type = 'locale', $exclude_used = false )
    {
        global
$locales;

       
$r = '';

       
$available_locales = $this->get_available_locales( $type );

        if(
$exclude_used )
        {    
// Exclude locales that are already used in the group of this Item:
           
$other_version_items = $this->get_other_version_items();
            foreach(
$other_version_items as $other_version_Item )
            {
                if( isset(
$available_locales[ $other_version_Item->get( 'locale' ) ] ) )
                {
                    unset(
$available_locales[ $other_version_Item->get( 'locale' ) ] );
                }
            }
        }

        if( empty(
$available_locales ) )
        {    
// No available locales:
           
return $r;
        }

       
$BlogCache = & get_BlogCache();

        foreach(
$available_locales as $locale_key => $linked_coll_ID )
        {
            if( ( isset(
$locales[ $locale_key ] ) && $locales[ $locale_key ]['enabled'] ) ||
               
$locale_key == $this->get( 'locale' ) )
            {    
// Allow enabled locales or if it is already selected for this Item:
               
if( ! empty( $linked_coll_ID ) )
                {    
// This is a linked locale from different collection:
                   
$locale_Blog = & $BlogCache->get_by_ID( $linked_coll_ID, false, false );
                }
                else
                {    
// Use collection of this Item:
                   
$locale_Blog = & $this->get_Blog();
                }
                if( !
$locale_Blog )
                {    
// Skip wrong locale:
                   
continue;
                }
               
$r .= '<option value="'.$locale_key.'"';
                if(
$locale_key == $this->get( 'locale' ) )
                {    
// This is a selected locale
                   
$r .= ' selected="selected"';
                }
               
$r .= ' data-coll-id="'.$locale_Blog->ID.'"';
               
$r .= ' data-coll-name="'.format_to_output( $locale_Blog->get( 'name' ), 'htmlattr' ).'"';
               
$r .= '>'.( isset( $locales[ $locale_key ] ) ? T_( $locales[ $locale_key ]['name'] ) : $locale_key ).'</option>'."\n";
            }
        }

        return
$r;
    }


   
/**
     * Set group ID from another Item
     *
     * @param integer ID of parent/source Item
     */
   
function set_group_ID( $parent_item_ID )
    {
       
$ItemCache = & get_ItemCache();
        if( ! (
$parent_Item = & $ItemCache->get_by_ID( $parent_item_ID, false, false ) ) )
        {    
// Wrong source Item ID:
           
return;
        }

        if( !
$parent_Item->get( 'igrp_ID' ) )
        {    
// Create new Item Group if it wasn't created yet:
           
global $DB;
            if(
$DB->query( 'INSERT INTO T_items__itemgroup () VALUES ()' ) )
            {
               
$parent_Item->set( 'igrp_ID', $DB->insert_id );
               
$parent_Item->dbupdate();
            }
        }

       
// Use the same Item Group as source Item has:
       
$this->set( 'igrp_ID', $parent_Item->get( 'igrp_ID' ) );
    }


   
/**
     * Get other version Items from the same group
     *
     * @param integer Include additional Item by ID, e.g. ID of source Item on adding new version
     * @return array
     */
   
function get_other_version_items( $source_item_ID = NULL )
    {
        if( ! isset(
$this->other_version_items ) )
        {    
// Try to load other version Items from DB:
           
if( $source_item_ID == $this->ID )
            {    
// Don't include the same Item:
               
$source_item_ID = NULL;
            }
           
$this->other_version_items = array();
            if( !
$this->get( 'igrp_ID' ) )
            {    
// No group for this Item yet:
               
if( ! empty( $source_item_ID ) )
                {    
// Include source Item:
                   
$ItemCache = & get_ItemCache();
                   
$ItemCache->clear();
                    if(
$Item = & $ItemCache->get_by_ID( $source_item_ID, false, false ) )
                    {
                       
$this->other_version_items[ $Item->ID ] = & $Item;
                    }
                }
            }
            else
            {    
// Load from DB:
               
global $DB;
               
$SQL = new SQL();
               
$SQL->SELECT( 'post_ID' );
               
$SQL->FROM( 'T_items__item' );
               
$SQL->WHERE( '(post_igrp_ID = '.$this->get( 'igrp_ID' ).( empty( $source_item_ID ) ? '' : ' OR post_ID = '.$source_item_ID ).')' );
                if(
$this->ID > 0 )
                {    
// Exclude this Item:
                   
$SQL->WHERE_and( 'post_ID != '.$this->ID );
                }
               
$group_item_IDs = $DB->get_col( $SQL );
                if(
count( $group_item_IDs ) )
                {
                   
$ItemCache = & get_ItemCache();
                   
$ItemCache->clear();
                   
$ItemCache->load_where( 'post_ID IN ( '.$DB->quote( $group_item_IDs ).' ) ' );
                    foreach(
$group_item_IDs as $group_item_ID )
                    {
                       
$this->other_version_items[ $group_item_ID ] = & $ItemCache->get_by_ID( $group_item_ID );
                    }
                }
            }
        }

        return
$this->other_version_items;
    }


   
/**
     * Get version Item by locale
     *
     * @param string Locale
     * @param boolean TRUE to check if the Item can be displayed for current User
     * @return object|NULL Item object
     */
   
function & get_version_Item( $locale, $check_visibility = true )
    {
       
$version_items = $this->get_other_version_items();
       
array_unshift( $version_items, $this );

        foreach(
$version_items as $version_Item )
        {
            if(
$version_Item->get( 'locale' ) == $locale &&
                ( !
$check_visibility || $version_Item->can_be_displayed() ) )
            {    
// Use first detected Item with requested locale and visible for current User:
               
return $version_Item;
            }
        }

       
$r = NULL;
        return
$r;
    }


   
/**
     * Check permission of current User to edit workflow properties
     *
     * @param string Permission name:
     *               - 'any' - Check to edit at least one workflow property
     *               - 'status', 'user', 'priority', 'deadline' - Check to edit one of these workflow properties
     * @param boolean Execution will halt if this is !0 and permission is denied
     */
   
function can_edit_workflow( $permname = 'any', $assert = false )
    {
       
$perm =
           
// Main Category must be defined for this Item in order to check permission in Collection of the Category:
           
! empty( $this->main_cat_ID ) &&
           
// Workflow must be enabled for current Collection:
           
$this->get_coll_setting( 'use_workflow' ) &&
           
// Current User must has a permission to be assigned for tasks of the current Collection:
           
check_user_perm( 'blog_can_be_assignee', 'edit', $assert, $this->get_blog_ID() );

        if(
$perm )
        {    
// Additional checking for several permissions when main checking is true:
           
switch( $permname )
            {
                case
'any':
                   
// Check if current User can edit at least one workflow property:
                   
$perm = check_user_perm( 'blog_workflow_status', 'edit', false, $this->get_blog_ID() ) ||
                       
check_user_perm( 'blog_workflow_user', 'edit', false, $this->get_blog_ID() ) ||
                       
check_user_perm( 'blog_workflow_priority', 'edit', false, $this->get_blog_ID() );
                    break;
                case
'deadline':
                   
// Deadline has additional collection setting to be enabled:
                   
$perm = $this->get_coll_setting( 'use_deadline' );
            }
        }

        if( !
$perm )
        {    
// No permission:
           
if( $assert )
            {    
// We can't let this go on!
               
global $app_name;
               
debug_die( sprintf( /* %s is the application name, usually "b2evolution" */ T_('Group/user permission denied by %s!'), $app_name ).' (Item#'.$this->ID.':workflow:'.( $permname === NULL ? 'MAIN' : $permname ).')' );
            }
            return
$perm;
        }

        switch(
$permname )
        {
            case
'any':
               
// Check if current User can edit at least one workflow property:
               
return $perm;
            case
'status':
               
// Check if current User can edit the workflow status:
               
return check_user_perm( 'blog_workflow_status', 'edit', $assert, $this->get_blog_ID() );
            case
'user':
               
// Check if current User can edit the workflow user:
               
return check_user_perm( 'blog_workflow_user', 'edit', $assert, $this->get_blog_ID() );
            case
'priority':
            case
'deadline':
               
// Check if current User can edit the workflow priority or deadline:
               
return check_user_perm( 'blog_workflow_priority', 'edit', $assert, $this->get_blog_ID() );
            default:
               
// Wrong request:
               
debug_die( 'Unhandled Item workflow permission name "'.$permname.'"' );
        }
    }


   
/**
     * Display workflow field to edit
     *
     * @param string Field key: 'status', 'user', 'priority', 'deadline'
     * @param object Form
     * @param array Additional parameters
     */
   
function display_workflow_field( $field, & $Form, $params = array() )
    {
        if( !
$this->can_edit_workflow( $field ) )
        {    
// Current User has no permission to edit the requested workflow property:
           
return;
        }

        switch(
$field )
        {
            case
'status':
               
$ItemStatusCache = & get_ItemStatusCache();
               
$ItemStatusCache->load_all();
               
$ItemTypeCache = & get_ItemTypeCache();
               
$current_ItemType = & $this->get_ItemType();
               
$Form->select_input_options( 'item_st_ID', $ItemStatusCache->get_option_list( $this->get( 'pst_ID' ), true, 'get_name', $current_ItemType->get_ignored_post_status() ), T_('Task status'), '', $params );
                break;

            case
'user':
               
// Load current blog members into cache:
               
$UserCache = & get_UserCache();
               
// Load only first 21 users to know when we should display an input box instead of full users list
               
$UserCache->load_blogmembers( $this->get_blog_ID(), 21, false );

                if(
count( $UserCache->cache ) > 20 )
                {
                   
$params = array_merge( array(
                           
'size' => 10,
                        ),
$params );
                   
$assigned_User = & $UserCache->get_by_ID( $this->get( 'assigned_user_ID' ), false, false );
                   
$Form->username( 'item_assigned_user_login', $assigned_User, T_('Assigned to'), '', 'only_assignees', $params );
                }
                else
                {
                   
$params = array_merge( array(
                           
'note' => '',
                           
'allow_none' => true,
                           
'class' => '',
                           
'object_callback' => 'get_assigned_user_options',
                        ),
$params );
                   
$Form->select_input_object( 'item_assigned_user_ID', NULL, $this, T_('Assigned to'), $params );
                }
                break;

            case
'priority':
               
$params = array_merge( array(
                       
'force_keys_as_values' => true,
                    ),
$params );
               
$Form->select_input_array( 'item_priority', $this->get( 'priority' ), item_priority_titles(), T_('Priority'), '', $params );
                break;

            case
'deadline':
                if(
$this->get_coll_setting( 'use_deadline' ) )
                {    
// Display deadline fields only if it is enabled for collection:
                   
$is_inline = isset( $Form->is_lined_fields ) && $Form->is_lined_fields;
                    if( !
$is_inline )
                    {
                       
$Form->begin_line( T_('Deadline'), 'item_deadline', '', $params );
                    }
                       
$date_params = array_merge( array(
                               
'input_suffix' => '&nbsp;'.T_('at').'&nbsp;',
                               
'placeholder'  => locale_input_datefmt(),
                            ),
$params );
                       
$datedeadline = $this->get( 'datedeadline' );
                       
$Form->date_input( 'item_deadline', $datedeadline, '', $date_params );

                       
$datedeadline_time = empty( $datedeadline ) ? '' : date( 'Y-m-d H:i', strtotime( $datedeadline ) );
                       
$time_params = array_merge( array(
                               
'time_format' => 'hh:mm',
                               
'placeholder' => 'hh:mm',
                               
'note'        => '',
                            ),
$params );
                       
$Form->time_input( 'item_deadline_time', $datedeadline_time, T_('at'), $time_params );
                    if( !
$is_inline )
                    {
                       
$Form->end_line();
                    }
                }
                break;
        }
    }


   
/**
     * Get ordered array of fields which can be edited on front-office
     *
     * @param array
     */
   
function get_front_edit_fields()
    {
       
$fields = array();

       
// Item fields which may be displayed on front-office:
       
$item_fields = array(
           
'title',
           
'short_title',
           
'instruction',
           
'attachments',
           
'workflow',
           
'text',
           
'tags',
           
'excerpt',
           
'url',
           
'location',
        );
        foreach(
$item_fields as $item_field )
        {
           
$fields[] = array(
               
'name'  => $item_field,
               
'order' => $this->get_type_setting( 'front_order_'.$item_field ),
               
'type'  => 'item',
            );
        }

       
// Custom fields:
       
$custom_fields = $this->get_custom_fields_defs();
        foreach(
$custom_fields as $custom_field )
        {
           
$fields[] = array(
               
'name'  => $custom_field['name'],
               
'public'=> $custom_field['public'],
               
'order' => $custom_field['order'],
               
'type'  => 'custom',
               
'value' => $custom_field['value'],
            );
        }

       
// Sort fields by order value:
       
usort( $fields, array( $this, 'sort_front_edit_fields_callback' ) );

        return
$fields;
    }


   
/**
     * Callback function to sort front edit fields by order value
     *
     * @param array Field data
     * @param array Field data
     * @return boolean
     */
   
function sort_front_edit_fields_callback( $a, $b )
    {
        if(
$a['order'] == $b['order'] )
        {    
// Sort by field type or name:
           
if( $a['type'] == $b['type'] )
            {    
// Sort by name when type is same:
               
return $a['name'] > $b['name'] ? 1 : -1;
            }
           
// Make item fields above custom fields:
           
return ( $a['type'] == 'custom' ? 1 : -1 );
        }

        return (
$a['order'] > $b['order'] ? 1 : -1 );
    }


   
/**
     * Do 302 redirect from tiny URL to canonical URL of this Item
     *
     * @param string Slug
     * @param string Slug extra term
     */
   
function tinyurl_redirect( $slug = NULL, $slug_extra_term = NULL )
    {
       
// Get item's canonical URL for redirect from tiny URL:
       
$redirect_to = $this->get_permanent_url( '', '', '&' );

        if(
is_pro() )
        {    
// Load extra code for extra processing:
           
load_funcs( '_core/_pro_features.funcs.php' );

           
// Get Collection of this Item:
           
$item_Blog = & $this->get_Blog();

           
// Add params for PRO version:
           
$redirect_to = pro_tinyurl_redirect_add_params( $redirect_to, $item_Blog, $slug, $slug_extra_term );
        }

       
// Keep ONLY allowed params from current URL in the canonical URL by configs AND Item's switchable params:
       
$redirect_to = url_keep_canonicals_params( $redirect_to, '&', array_keys( $this->get_switchable_params() ) );

       
header_redirect( $redirect_to, 302 );  // 302 is easier for debugging; TODO: setting to choose type of redirect
   
}


   
/**
     * Get info for form field selector
     *
     * @return string
     */
   
function get_form_selector_info()
    {
       
$r = '';

       
$status_icons = get_visibility_statuses( 'icons' );
        if( isset(
$status_icons[ $this->get( 'status' ) ] ) )
        {    
// Status colored icon:
           
$r .= $status_icons[ $this->get( 'status' ) ];
        }
       
// Title with link to permament url:
       
$r .= ' '.$this->get_title( array( 'link_type' => 'permalink' ) );
       
// Icon to edit if current User has a permission:
       
$r .= ' '.$this->get_edit_link( array( 'text' => '#icon#' ) );

        return
$r;
    }
}
?>