<?php
/**
 * This file implements the User 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.' );


// DEBUG: (Turn switch on or off to log debug info for specified category)
$GLOBALS['debug_perms'] = false;


load_class( '_core/model/dataobjects/_dataobject.class.php', 'DataObject' );

/**
 * User Class
 *
 * @package evocore
 */
class User extends DataObject
{
	//var $postcode;
	var $age_min;
	var $age_max;
	var $login;
	var $pass;
	var $salt;
	var $pass_driver;
	var $firstname;
	var $lastname;
	var $nickname;
	var $locale;
	var $email;
	var $url;
	var $datecreated;
	var $lastseen_ts;
	var $level;
	var $avatar_file_ID;
	var $reg_ctry_ID;
	var $ctry_ID;
	var $rgn_ID;
	var $subrg_ID;
	var $city_ID;
	var $source;
	var $unsubscribe_key;
	var $gender;
	var $email_dom_ID;
	var $profileupdate_date;
	var $birthday_year;
	var $birthday_month;
	var $birthday_day;

	/**
	 * User account status
	 *
	 * 'new', 'activated', 'manualactivated', 'autoactivated', 'emailchanged', 'deactivated', 'failedactivation', 'pendingdelete', 'closed'
	 *
	 * @var string
	 */
	var $status;
	/**
	 * User previous status. It will be set only if the user status was changed.
	 * @var string
	 */
	var $previous_status;

	/**
	 * Number of posts by this user. Use get_num_posts() to access this (lazy filled).
	 * @var array ( key = status name, value = number of posts with status, key = '' - total number with all statuses )
	 * @access protected
	 */
	var $_num_posts;

	/**
	 * Number of comments by this user. Use get_num_comments() to access this (lazy filled).
	 * @var array  ( key = status name, value = number of comments with status, key = '' - total number with all statuses )
	 * @access protected
	 */
	var $_num_comments;

	/**
	 * Number of files by this user. Use get_num_files() to access this (lazy filled).
	 * @var array  ( key = type, value = number of files with type )
	 * @access protected
	 */
	var $_num_files;

	/**
	 * The ID of the (primary, currently only) group of the user.
	 * @var integer
	 */
	var $grp_ID;

	/**
	 * Reference to group
	 * @see User::get_Group()
	 * @var Group
	 * @access protected
	 */
	var $Group;

	/**
	 * Array of the references to groups
	 * @see User::get_secondary_groups()
	 * @var secondary_groups
	 * @access protected
	 */
	var $secondary_groups;

	/**
	 * Country lazy filled
	 *
	 * @var country
	 */
	var $Country;
	var $Region;
	var $Subregion;
	var $City;

	/**
	 * Blog posts statuses permissions
	 */
	var $blog_post_statuses = array();

	/**
	 * Cache for perms.
	 * @access protected
	 * @var array
	 */
	var $cache_perms = array();


	/**
	 * User fields
	 */
	var $userfields_loaded = false;
	var $userfields = array();
	var $userfields_by_code = array();
	var $userfields_by_type = array();
	var $updated_fields = array();
	var $new_fields = array();

	/**
	 * Userfield defs
	 */
	var $userfield_defs;

	/**
	 * The previous values of significant fields which were changed and these changes should be notified.
	 */
	var $significant_changed_values = array();

	/**
	 * TRUE if user is owner of at least one collection
	 * @access protected
	 * @var boolean
	 */
	var $is_collection_owner;

	/**
	 * Password driver
	 * @var object
	 */
	var $PasswordDriver;

	/**
	 * IDs of newsletters which this user is subscribed to
	 * @var array
	 */
	var $newsletter_subscriptions;

	/**
	 * TRUE if user's newsletter subscriptions were updated during user updating
	 * @var boolean
	 */
	var $newsletter_subscriptions_updated = false;

	/**
	 * Array of newsletter subscription changes with the following elements:
	 *   - string, 'subscribe' | 'unsubscribe'
	 *   - array of newsletter IDs
	 *   - array of email template params
	 * @var array
	 */
	var $newsletter_subscription_changed_values = array();

	/**
	 * Constructor
	 *
	 * @param object DB row
	 */
	function __construct( $db_row = NULL )
	{
		global $default_locale, $Settings, $localtimenow;

		// Call parent constructor:
		parent::__construct( 'T_users', 'user_', 'user_ID' );

		if( $db_row == NULL )
		{ // Setting those object properties, which are not "NULL" in DB (MySQL strict mode):

			// echo 'Creating blank user';
			$this->set( 'login', 'login' );
			$this->set_password( 'pass' );
			$this->set( 'locale',
				isset( $Settings )
					? $Settings->get('default_locale') // TODO: (settings) use "new users template setting"
					: $default_locale );
			$this->set( 'email', '' );	// fp> TODO: this is an invalid value. Saving the object without a valid email should fail! (actually: it should be fixed by providing a valid email)
			$this->set( 'level', isset( $Settings ) ? $Settings->get('newusers_level') : 0 );
			if( isset($localtimenow) )
			{
				$this->set_datecreated( $localtimenow );
				$this->set( 'profileupdate_date', date( 'Y-m-d H:i:s', $localtimenow ) );
			}
			else
			{ // We don't know local time here!
				$this->set_datecreated( time() );
				$this->set( 'profileupdate_date', date( 'Y-m-d H:i:s' ) );
			}

			if( isset( $Settings ) )
			{ // Set Group for this user:
				$GroupCache = & get_GroupCache();
				$new_user_Group = & $GroupCache->get_by_ID( $Settings->get( 'newusers_grp_ID' ) );
				$this->set_Group( $new_user_Group );
				// set status for this user
				$this->set( 'status', $Settings->get('newusers_mustvalidate') ? 'new' : 'autoactivated' );
			}
			else
			{
				// set status for this user
				$this->set( 'status', 'new' );
			}

			$this->set( 'unsubscribe_key', generate_random_key() );
		}
		else
		{
			// echo 'Instanciating existing user';
			$this->ID = $db_row->user_ID;
			$this->age_min = $db_row->user_age_min;
			$this->age_max = $db_row->user_age_max;
			$this->login = $db_row->user_login;
			$this->pass = $db_row->user_pass;
			$this->salt = $db_row->user_salt;
			$this->pass_driver = $db_row->user_pass_driver;
			$this->firstname = $db_row->user_firstname;
			$this->lastname = $db_row->user_lastname;
			$this->nickname = $db_row->user_nickname;
			$this->locale = $db_row->user_locale;
			$this->email = $db_row->user_email;
			$this->status = $db_row->user_status;
			$this->url = $db_row->user_url;
			$this->datecreated = $db_row->user_created_datetime;
			$this->lastseen_ts = $db_row->user_lastseen_ts;
			$this->level = $db_row->user_level;
			$this->unsubscribe_key = $db_row->user_unsubscribe_key;
			$this->gender = $db_row->user_gender;
			$this->avatar_file_ID = $db_row->user_avatar_file_ID;
			$this->reg_ctry_ID = $db_row->user_reg_ctry_ID;
			$this->ctry_ID = $db_row->user_ctry_ID;
			$this->rgn_ID = $db_row->user_rgn_ID;
			$this->subrg_ID = $db_row->user_subrg_ID;
			$this->city_ID = $db_row->user_city_ID;
			$this->source = $db_row->user_source;
			$this->profileupdate_date = $db_row->user_profileupdate_date;
			$this->birthday_year = $db_row->user_birthday_year;
			$this->birthday_month = $db_row->user_birthday_month;
			$this->birthday_day = $db_row->user_birthday_day;

			// Group for this user:
			$this->grp_ID = $db_row->user_grp_ID;
		}
	}


	/**
	 * Get this class db table config params
	 *
	 * @return array
	 */
	static function get_class_db_config()
	{
		static $user_db_config;

		if( !isset( $user_db_config ) )
		{
			$user_db_config = array_merge( parent::get_class_db_config(),
				array(
					'dbtablename'        => 'T_users',
					'dbprefix'           => 'user_',
					'dbIDname'           => 'user_ID',
				)
			);
		}

		return $user_db_config;
	}


	/**
	 * Get delete restriction settings
	 *
	 * @return array
	 */
	static function get_delete_restrictions()
	{
		// fp> These settings should probably be merged with the global database description used by the installer/upgrader. However I'm not sure about how compelx plugins would be able to integrate then...
		// asimo> Delete cascades and restrictions handling can't be handled globally in all circumstances because in a few places there is a logic behind the deletion:
		//        e.g. Delete user as spammer or simply delete the user
		// asimo> For sure in those cases when the cascade has no condition it would be faster to handled in on the db level.
		//        The current db_delete_where solution handles the delete cascade generally and handle the speciel cases in the cascaded classes as well ( e.g. delete files )
		return array(
				array( 'table'=>'T_blogs', 'fk'=>'blog_owner_user_ID', 'msg'=>T_('%d blogs owned by this user') ),
				//array( 'table'=>'T_items__item', 'fk'=>'post_lastedit_user_ID', 'msg'=>T_('%d posts last edited by this user') ),
				array( 'table'=>'T_items__item', 'fk'=>'post_assigned_user_ID', 'msg'=>T_('%d posts assigned to this user') ),
				array( 'table'=>'T_users__organization', 'fk'=>'org_owner_user_ID', 'msg'=>T_('%d organizations') ),
				array( 'table'=>'T_polls__question', 'fk'=>'pqst_owner_user_ID', 'msg'=>T_('%d polls owned by this user') ),
				array( 'table'=>'T_automation__automation', 'fk'=>'autm_owner_user_ID', 'msg'=>T_('%d automations') ),
				// Do not delete user private messages
				//array( 'table'=>'T_messaging__message', 'fk'=>'msg_author_user_ID', 'msg'=>T_('The user has authored %d message(s)') ),
				//array( 'table'=>'T_messaging__threadstatus', 'fk'=>'tsta_user_ID', 'msg'=>T_('The user is part of %d messaging thread(s)') ),
			);
	}


	/**
	 * Get delete cascade settings
	 *
	 * @return array
	 */
	static function get_delete_cascades()
	{
		return array(
				array( 'table'=>'T_users__usersettings', 'fk'=>'uset_user_ID', 'msg'=>T_('%d user settings on collections') ),
				array( 'table'=>'T_sessions', 'fk'=>'sess_user_ID', 'msg'=>T_('%d sessions opened by this user'),
						'class'=>'Session', 'class_path'=>'sessions/model/_session.class.php' ),
				array( 'table'=>'T_coll_user_perms', 'fk'=>'bloguser_user_ID', 'msg'=>T_('%d user permissions on blogs') ),
				array( 'table'=>'T_comments__votes', 'fk'=>'cmvt_user_ID', 'msg'=>T_('%d user votes on comments') ),
				array( 'table'=>'T_subscriptions', 'fk'=>'sub_user_ID', 'msg'=>T_('%d blog subscriptions') ),
				array( 'table'=>'T_items__item', 'fk'=>'post_creator_user_ID', 'msg'=>T_('%d posts created by this user'),
						'class'=>'Item', 'class_path'=>'items/model/_item.class.php', 'style' => 'bold' ),
				array( 'table'=>'T_items__subscriptions', 'fk'=>'isub_user_ID', 'msg'=>T_('%d post subscriptions') ),
				array( 'table'=>'T_messaging__contact', 'fk'=>'mct_to_user_ID', 'msg'=>T_('%d contacts from other users contact list') ),
				array( 'table'=>'T_messaging__contact', 'fk'=>'mct_from_user_ID', 'msg'=>T_('%d contacts from this user contact list') ),
				array( 'table'=>'T_messaging__contact_groups', 'fk'=>'cgr_user_ID', 'msg'=>T_('%d contact groups') ),
				array( 'table'=>'T_messaging__contact_groupusers', 'fk'=>'cgu_user_ID', 'msg'=>T_('%d contacts from contact groups') ),
				array( 'table'=>'T_pluginusersettings', 'fk'=>'puset_user_ID', 'msg'=>T_('%d user settings on plugins') ),
				array( 'table'=>'T_users__fields', 'fk'=>'uf_user_ID', 'msg'=>T_('%d user fields') ),
				array( 'table'=>'T_items__user_data', 'fk'=>'itud_user_ID', 'msg'=>T_('%d recordings of user data for a specific post') ),
				array( 'table'=>'T_links', 'fk'=>'link_usr_ID', 'msg'=>T_('%d links to this user'),
						'class'=>'Link', 'class_path'=>'links/model/_link.class.php' ),
				array( 'table'=>'T_files', 'fk'=>'file_root_ID', 'and_condition'=>'file_root_type = "user"', 'msg'=>T_('%d files from this user file root'), 'style' => 'bold' ),
				array( 'table'=>'T_email__campaign_send', 'fk'=>'csnd_user_ID', 'msg'=>T_('%d list emails for this user') ),
				array( 'table'=>'T_users__reports', 'fk'=>'urep_target_user_ID', 'msg'=>T_('%d reports about this user') ),
				array( 'table'=>'T_users__reports', 'fk'=>'urep_reporter_ID', 'msg'=>T_('%d reports created by this user') ),
				array( 'table'=>'T_users__user_org', 'fk'=>'uorg_user_ID', 'msg'=>T_('%d organization membership') ),
				array( 'table'=>'T_users__usertag', 'fk'=>'uutg_user_ID', 'msg'=>T_('%d links to tags') ),
				array( 'table'=>'T_polls__answer', 'fk'=>'pans_user_ID', 'msg'=>T_('%d poll answers') ),
				array( 'table'=>'T_users__secondary_user_groups', 'fk'=>'sug_user_ID', 'msg'=>T_('%d secondary groups') ),
				array( 'table'=>'T_users__profile_visits', 'fk'=>'upv_visited_user_ID', 'msg'=>T_('%d profile visits') ),
				array( 'table'=>'T_users__profile_visit_counters', 'fk'=>'upvc_user_ID', 'msg'=>T_('%d profile visit counter' ) ),
				array( 'table'=>'T_users__social_network', 'fk'=>'usn_user_ID', 'msg'=>T_('%d connected social networks') ),
				array( 'table'=>'T_email__newsletter_subscription', 'fk'=>'enls_user_ID', 'msg'=>T_('%d list subscriptions') ),
				array( 'table'=>'T_automation__user_state', 'fk'=>'aust_user_ID', 'msg'=>T_('%d states of User in Automation') ),
			);
	}


	/**
	 * Initialize relations for restrict and cascade deletion.
	 * Spammer user's deletion will cascade more items.
	 *
	 * @param boolean Is this user spammer
	 */
	function init_relations( $params = array() )
	{
		if( ! is_null( $this->delete_cascades ) || ! is_null( $this->delete_restrictions ) )
		{ // Initialize the relations only once
			return;
		}

		$params = array_merge( array(
				'delete_messages' => false,
				'delete_comments' => false,
				'delete_files'    => false,
			), $params );

		parent::init_relations();

		if( $params['delete_messages'] )
		{	// Delete messages:
			$this->delete_cascades[] = array( 'table'=>'T_messaging__message', 'fk'=>'msg_author_user_ID', 'msg'=>T_('%d messages from this user'),
					'class'=>'Message', 'class_path'=>'messaging/model/_message.class.php', 'style' => 'bold' );
			$this->delete_cascades[] = array( 'table'=>'T_messaging__threadstatus', 'fk'=>'tsta_user_ID', 'msg'=>T_('%d message read statuses from this user') );
		}
		if( $params['delete_comments'] )
		{	// Delete comments:
			$this->delete_cascades[] = array( 'table'=>'T_comments', 'fk'=>'comment_author_user_ID', 'msg'=>T_('%d comments by this user'),
					'class'=>'Comment', 'class_path'=>'comments/model/_comment.class.php', 'style' => 'bold' );
		}
		if( $params['delete_files'] )
		{	// Delete files and links:
			$this->delete_cascades[] = array( 'table'=>'T_links', 'fk'=>'link_creator_user_ID', 'msg'=>T_('%d links created by this user'),
					'class'=>'Link', 'class_path'=>'links/model/_link.class.php' );
			$this->delete_cascades[] = array( 'table'=>'T_files', 'fk'=>'file_creator_user_ID', 'and_condition'=>'file_root_type != "user"', 'msg'=>T_('%d files will lose their creator ID.') );
		}
	}


	/**
	 * Get link to restricted object
	 *
	 * Used when try to delete an user which has at least one poll question
	 *
	 * @param array restriction
	 * @return string|boolean Message with link to objects,
	 *                        Empty string if no restriction for current table,
	 *                        FALSE - if no rule for current table
	 */
	function get_restriction_link( $restriction )
	{
		if( $restriction['table'] == 'T_polls__question' )
		{	// Check restriction for poll questions:
			global $DB, $admin_url;

			// Get a count of poll questions
			$polls_count_SQL = new SQL();
			$polls_count_SQL->SELECT( 'COUNT( pqst_ID )' );
			$polls_count_SQL->FROM( $restriction['table'] );
			$polls_count_SQL->WHERE( $restriction['fk'].' = '.$this->ID );
			$polls_count = $DB->get_var( $polls_count_SQL->get(), 0, NULL, 'Get all poll questions of the user to check restriction befor deleting' );

			if( $polls_count > 0 )
			{	// Display this restriction as link to polls list with filter by this user login:
				$msg = sprintf( $restriction['msg'], $polls_count );
				if( check_user_perm( 'polls', 'view' ) )
				{	// Set a link to poll questions list with filter by this user if current user has a perm to view all polls:
					$msg = '<a href="'.$admin_url.'?ctrl=polls&amp;owner='.$this->login.'">'.$msg.'</a>';
				}
				return $msg;
			}
			else
			{	// No restriction for current table:
				return '';
			}
		}

		// No rule for current table:
		return false;
	}


	/**
	 * Check relations for restrictions before deleting
	 *
	 * @param string
	 * @param array list of foreign keys to ignore
	 * @param boolean TRUE to display restriction as link to edit the record
	 * @return boolean true if no restriction prevents deletion
	 */
	function check_delete( $restrict_title, $ignore = array(), $addlink = false )
	{
		// Prepend user login before restriction messages to know what user is deleting:
		$user_login_link = $this->get_identity_link( array(
				'link_text' => 'login',
			) );

		return parent::check_delete( $user_login_link.': '.$restrict_title, $ignore, true );
	}


	/**
	 * Load data from registration form
	 *
	 * @return boolean true if loaded data seems valid.
	 */
	function load_from_register_form()
	{
		global $DB, $Settings, $UserSettings, $GroupCache, $Messages, $action;
		global $current_User, $Session, $localtimenow;
		global $is_api_request;

		// TRUE when we create new user:
		$is_new_user = ( $this->ID == 0 );

		// TRUE if we should request a second password to confirm:
		// (Don't request it when we create new user from REST API)
		$request_password_confirmation = ! ( $is_api_request && $is_new_user );

		$has_full_access = check_user_perm( 'users', 'edit' );

		// ---- Login checking / START ----
		$edited_user_login = $this->check_login();
		// ---- Login checking / END ----

		// ---- Password checking / START ----
		// TODO: Must be factorized with code from $this->load_from_Request()!
		$reqID = param( 'reqID', 'string', '' );

		global $edited_user_pass1, $edited_user_pass2;

		$edited_user_pass1 = param( 'edited_user_pass1', 'string', true );
		// Remove the invalid chars from password var:
		$edited_user_pass1 = preg_replace( '/[<>&]/', '', $edited_user_pass1 );

		if( $request_password_confirmation )
		{	// Request a password confirmation:
			$edited_user_pass2 = param( 'edited_user_pass2', 'string', true );
			// Remove the invalid chars from password var:
			$edited_user_pass2 = preg_replace( '/[<>&]/', '', $edited_user_pass2 );
		}

		if( $is_new_user ||
				( ! empty( $reqID ) && $reqID == $Session->get( 'core.changepwd.request_id' ) ) ||
				( $this->get( 'pass_driver' ) == 'nopass' ) )
		{	// Current password is not required:
			//   - new user creating process
			//   - password change requested by email
			//   - password has not been set yet(email capture/quick registration)

			if( $request_password_confirmation )
			{	// Request a password confirmation:
				if( param_check_passwords( 'edited_user_pass1', 'edited_user_pass2', true, $Settings->get('user_minpwdlen') ) )
				{ // We can set password
					$this->set_password( $edited_user_pass2 );
				}
			}
			else
			{	// Don't request a password confirmation and use only first entered password (Used on REST API):
				$this->set_password( $edited_user_pass1 );
			}
		}
		else
		{
			// ******* Password edit form ****** //

			$current_user_pass = param( 'current_user_pass', 'string', true );

			if( $this->ID != $current_User->ID )
			{ // Set the messages when admin changes a password of other user
				$checkpwd_params = array(
						'msg_pass_new'   => T_('Please enter new password.'),
						'msg_pass_twice' => T_('Please enter new password twice.'),
					);
			}
			else
			{ // Use default messages
				$checkpwd_params = array();
			}

			if( ! strlen( $current_user_pass ) )
			{
				param_error( 'current_user_pass' , T_('Please enter your current password.') );
				param_check_passwords( 'edited_user_pass1', 'edited_user_pass2', true, $Settings->get('user_minpwdlen'), $checkpwd_params );
			}
			else
			{
				if( $has_full_access && $this->ID != $current_User->ID )
				{	// Admin is changing a password of other user, Check a password of current admin
					$pass_User = $current_User;
				}
				else
				{	// User is changing own password
					$pass_User = $this;
				}
				if( $pass_User->check_password( $current_user_pass ) )
				{
					if( param_check_passwords( 'edited_user_pass1', 'edited_user_pass2', true, $Settings->get('user_minpwdlen'), $checkpwd_params ) )
					{ // We can set password
						$this->set_password( $edited_user_pass2 );
					}
				}
				else
				{
					param_error('current_user_pass' , T_('Your current password is incorrect.') );
					param_check_passwords( 'edited_user_pass1', 'edited_user_pass2', true, $Settings->get('user_minpwdlen'), $checkpwd_params );
				}
			}

		}

		if( $_POST['edited_user_pass1'] != trim( $_POST['edited_user_pass1'] ) ||
				$_POST['edited_user_pass2'] != trim( $_POST['edited_user_pass2'] ) )
		{ // If new password was entered with spaces then inform user about this
			$Messages->add( T_('The leading and traling spaces have been trimmed from the new password.'), 'warning' );
		}
		// ---- Password checking / END ----

		// Email is always required:
		$edited_user_email = utf8_strtolower( param( 'edited_user_email', 'string', true ) );
		param_check_not_empty( 'edited_user_email', T_('Please enter your e-mail address.') );
		param_check_email( 'edited_user_email', true );
		$this->set_email( $edited_user_email );

		// Other fields:
		$country = param( 'country', 'integer', '' );
		$firstname = param( 'firstname', 'string', '' );
		$lastname = param( 'lastname', 'string', NULL );
		$gender = param( 'gender', 'string', NULL );
		$locale = param( 'locale', 'string', '' );

		// Set params:
		$required_fields = get_registration_template_required_fields();

		$paramsList = array();
		if( in_array( 'country', $required_fields ) )
		{	// Set and check country:
			$this->set( 'ctry_ID', $country );
			$paramsList['country'] = $country;
		}
		if( in_array( 'firstname', $required_fields ) )
		{	// Set and check first name:
			$this->set( 'firstname', $firstname );
			$paramsList['firstname'] = $firstname;
		}
		if( in_array( 'lastname', $required_fields ) )
		{	// Set last name:
			$this->set( 'lastname', $lastname );
			$paramsList['lastname'] = $lastname;
		}
		if( in_array( 'gender', $required_fields ) )
		{	// Set or check gender:
			$this->set( 'gender', $gender );
			$paramsList['gender'] = $gender;
		}
		if( in_array( 'country', $required_fields ) )
		{	// Only update locale without checking:
			$this->set( 'locale', $locale );
		}
		// Check profile params:
		profile_check_params( $paramsList );

		return ! param_errors_detected();
	}


	/**
	 * Load data from Request form fields.
	 *
	 * @return boolean true if loaded data seems valid.
	 */
	function load_from_Request()
	{
		global $DB, $Settings, $UserSettings, $GroupCache, $Messages, $action;
		global $current_User, $Session, $localtimenow;
		global $is_api_request;

		$has_full_access = check_user_perm( 'users', 'edit' );
		$has_moderate_access = $current_User->can_moderate_user( $this->ID );

		// TRUE when we create new user:
		$is_new_user = ( $this->ID == 0 );

		// TRUE if we should request a second password to confirm:
		// (Don't request it when we create new user from REST API)
		$request_password_confirmation = ! ( $is_api_request && $is_new_user );

		// ---- Login checking / START ----
		$edited_user_login = $this->check_login();
		// ---- Login checking / END ----

		$is_identity_form = param( 'identity_form', 'boolean', false );
		$is_admin_form = param( 'admin_form', 'boolean', false );

		// ******* Admin form or new user create ******* //
		// In both cases current user must have users edit permission!
		if( ( $is_admin_form || ( $is_identity_form && $is_new_user ) || ( $is_api_request && $is_new_user ) ) && $has_moderate_access )
		{ // level/group and email options are displayed on identity form only when creating a new user.
			if( $this->ID != 1 )
			{ // the admin user group can't be changed
				$edited_user_level = param( 'edited_user_level', 'integer', ! $is_api_request ? true : NULL );
				if( isset( $edited_user_level ) )
				{
					param_integer_range( 'edited_user_level', 0, 10, T_('User level must be between %d and %d.'), true );
					$this->set_from_Request( 'level', 'edited_user_level', true );
				}

				$edited_user_Group = param( 'edited_user_grp_ID', 'integer' );
				if( isset( $edited_user_Group ) )
				{
					$edited_user_Group = $GroupCache->get_by_ID( $edited_user_Group );
					if( $edited_user_Group->can_be_assigned() )
					{	// Update group only if current user has a permission for this:
						$this->set_Group( $edited_user_Group );
					}
				}
			}

			$edited_user_secondary_grp_IDs = param( 'edited_user_secondary_grp_ID', 'array:integer', NULL );
			if( $edited_user_secondary_grp_IDs !== NULL )
			{	// Save secondary groups for this user:
				// Store old groups in order to delete them before insert new selected groups:
				$this->old_secondary_groups = $this->get_secondary_groups();
				$GroupCache = & get_GroupCache();
				$GroupCache->clear();
				// Set new groups which should be stored in DB:
				$this->secondary_groups = $GroupCache->load_list( $edited_user_secondary_grp_IDs );
			}

			$edited_user_source = param( 'edited_user_source', 'string', ! $is_api_request ? true : NULL );
			if( isset( $edited_user_source ) )
			{
				$this->set_from_Request('source', 'edited_user_source', true);
			}

			// set email, without changing the user status
			$edited_user_email = param( 'edited_user_email', 'string', ( ( ! $is_api_request ) || ( $is_api_request && $is_new_user ) ) ? true : NULL );
			if( isset( $edited_user_email ) )
			{
				$edited_user_email = utf8_strtolower( $edited_user_email );
				param_check_not_empty( 'edited_user_email', T_('Please enter your e-mail address.') );
				param_check_email( 'edited_user_email', true );
				$this->set_email( $edited_user_email, false );
			}

			if( $is_admin_form )
			{	// Admin form
				$notification_sender_email = param( 'notification_sender_email', 'string', ! $is_api_request ? true : NULL );
				if( isset( $notification_sender_email ) )
				{
					$notification_sender_email = utf8_strtolower( $notification_sender_email );
					param_check_email( 'notification_sender_email' );
					if( ! empty( $notification_sender_email ) )
					{ // Change a value of setting
						$UserSettings->set( 'notification_sender_email', $notification_sender_email, $this->ID );
					}
					elseif( $UserSettings->get( 'notification_sender_email' , $this->ID ) != '' )
					{ // Delete a setting record from DB
						$UserSettings->delete( 'notification_sender_email', $this->ID );
					}
				}

				$notification_sender_name = param( 'notification_sender_name', 'string', ! $is_api_request ? true : NULL );
				if( isset( $notification_sender_name ) )
				{
					if( ! empty( $notification_sender_name ) )
					{ // Change a value of setting
						$UserSettings->set( 'notification_sender_name', $notification_sender_name, $this->ID );
					}
					elseif( $UserSettings->get( 'notification_sender_name' , $this->ID ) != '' )
					{ // Delete a setting record from DB
						$UserSettings->delete( 'notification_sender_name', $this->ID );
					}
				}

				if( $has_full_access && !isset( $this->dbchanges['user_email'] ) )
				{	// If email address is not changed
					// Update status of email address in the T_email_address table
					$edited_email_status = param( 'edited_email_status', 'string' );
					$EmailAddressCache = & get_EmailAddressCache();
					$EmailAddress = & $EmailAddressCache->get_by_name( $this->get( 'email' ), false, false );
					if( !$EmailAddress && $edited_email_status != 'unknown' )
					{	// Create new record in the T_email_address table
						$EmailAddress = new EmailAddress();
						$EmailAddress->set( 'address', $this->get( 'email' ) );
					}
					if( !empty( $EmailAddress ) )
					{	// Save status of an email address
						$EmailAddress->set( 'status', $edited_email_status );
						$EmailAddress->dbsave();
					}
				}

				if( check_user_perm( 'spamblacklist', 'edit' ) )
				{ // User can edit IP ranges
					// Update status of IP range in DB
					$edited_iprange_status = param( 'edited_iprange_status', 'string' );
					$IPRangeCache = & get_IPRangeCache();
					$IPRange = & $IPRangeCache->get_by_ip( int2ip( $UserSettings->get( 'created_fromIPv4', $this->ID ) ) );
					if( !$IPRange && !empty( $edited_iprange_status ) )
					{ // IP range doesn't exist in DB, Create new record
						$ip_24bit_start = ip2int( preg_replace( '#\.\d{1,3}$#i', '.0', int2ip( $UserSettings->get( 'created_fromIPv4', $this->ID ) ) ) );
						$ip_24bit_end = ip2int( preg_replace( '#\.\d{1,3}$#i', '.255', int2ip( $UserSettings->get( 'created_fromIPv4', $this->ID ) ) ) );
						$IPRange = new IPRange();
						$IPRange->set( 'IPv4start', $ip_24bit_start );
						$IPRange->set( 'IPv4end', $ip_24bit_end );
						$IPRange->set( 'user_count', 1 );
					}
					if( $IPRange )
					{ // Save status of IP range
						if( $IPRange->get( 'status' ) != 'blocked' && $edited_iprange_status == 'blocked' )
						{	// Status was changed to blocked, we should increase counter
							$IPRange->set( 'block_count', $IPRange->block_count + 1 );
						}
						else if( $IPRange->get( 'status' ) == 'blocked' && $edited_iprange_status != 'blocked' )
						{ // Status was changed from blocked to another, we should decrease counter
							$IPRange->set( 'block_count', $IPRange->block_count - 1 );
						}
						$IPRange->set( 'status', $edited_iprange_status, true );
						$IPRange->dbsave();
					}
				}

				if( check_user_perm( 'stats', 'edit' ) )
				{ // User can edit Domains
					$DomainCache = & get_DomainCache();

					load_funcs('sessions/model/_hitlog.funcs.php');

					// Update status of email Domain in DB:
					$edited_email_domain_status = param( 'edited_email_domain_status', 'string' );
					$email_domain = $this->get_email_domain();
					$Domain = & get_Domain_by_subdomain( $email_domain );
					if( ! $Domain && $edited_email_domain_status != 'unknown' && ! empty( $email_domain ) )
					{	// Domain doesn't exist in DB, Create new record:
						$Domain = new Domain();
						$Domain->set( 'name', $email_domain );
					}
					if( $Domain )
					{	// Save status of Domain:
						$Domain->set( 'status', $edited_email_domain_status, true );
						$Domain->dbsave();
					}

					// Update status of user Domain in DB:
					$edited_domain_status = param( 'edited_domain_status', 'string' );
					$user_domain = $UserSettings->get( 'user_registered_from_domain', $this->ID );
					$Domain = & get_Domain_by_subdomain( $user_domain );
					if( ! $Domain && $edited_domain_status != 'unknown' && ! empty( $user_domain ) )
					{ // Domain doesn't exist in DB, Create new record
						$Domain = new Domain();
						$Domain->set( 'name', $user_domain );
					}
					if( $Domain )
					{ // Save status of Domain
						$Domain->set( 'status', $edited_domain_status, true );
						$Domain->dbsave();
					}

					// Update status of Initial referer in DB
					$edited_initial_referer_status = param( 'edited_initial_referer_status', 'string' );
					$initial_referer = $UserSettings->get( 'initial_referer', $this->ID );
					$initial_referer_domain = url_part( $initial_referer, 'host' );
					$Domain = & get_Domain_by_url( $initial_referer );
					if( ! $Domain && $edited_initial_referer_status != 'unknown' && ! empty( $initial_referer_domain ) )
					{ // Domain doesn't exist in DB, Create new record
						$Domain = new Domain();
						$Domain->set( 'name', $initial_referer_domain );
					}
					if( $Domain )
					{ // Save status of Domain
						$Domain->set( 'status', $edited_initial_referer_status, true );
						$Domain->dbsave();
					}
				}
			}
		}

		if( $has_moderate_access )
		{	// If current user can moderate other users:
			if( param( 'edited_user_tags', 'string', NULL ) !== NULL )
			{	// Update user tags if they has been submitted:
				$this->set_usertags_from_string( get_param( 'edited_user_tags' ) );
			}
		}

		// ******* Identity form ******* //
		if( $is_identity_form || ( $is_api_request && $is_new_user ) )
		{
			$edited_user_perms = array( 'edited-user', 'edited-user-required' );

			if( $Settings->get( 'self_selected_age_group') != 'hidden' )
			{
				global $edited_user_age_min, $edited_user_age_max;
				$age_group_is_required = $Settings->get( 'self_selected_age_group' ) == 'required';
				$age_min = param( 'edited_user_age_min', 'integer', ! $is_api_request || $age_group_is_required ? true : NULL );
				$age_max = param( 'edited_user_age_max', 'integer', ! $is_api_request || $age_group_is_required ? true : NULL );
				if( isset( $age_min ) || isset( $age_max ) || $is_identity_form )
				{
					param_check_interval( 'edited_user_age_min', 'edited_user_age_max', T_('Age must be a number.'), T_('The first age must be lower than (or equal to) the second.'), $age_group_is_required );
					if( !param_has_error( 'edited_user_age_min' ) && $Settings->get( 'minimum_age' ) > 0 &&
							!empty( $edited_user_age_min ) && $edited_user_age_min < $Settings->get( 'minimum_age' ) )
					{	// Limit user by minimum age
						param_error( 'edited_user_age_min', sprintf( T_('You must be at least %d years old to use this service.'), $Settings->get( 'minimum_age' ) ) );
					}
					$this->set_from_Request( 'age_min', 'edited_user_age_min', true );
					$this->set_from_Request( 'age_max', 'edited_user_age_max', true );
				}
			}

			$birthdate = array();
			if( $Settings->get( 'birthday_month' ) != 'hidden' )
			{	// Save Birthday Month
				$birthday_month_is_required = $Settings->get( 'birthday_month' ) == 'required';
				$edited_user_birthday_month = param( 'edited_user_birthday_month', 'integer', ! $is_api_request || $birthday_month_is_required ? true : NULL );
				if( isset( $edited_user_birthday_month ) || $is_identity_form )
				{
					if( $birthday_month_is_required && $has_moderate_access && empty( $edited_user_birthday_month ) )
					{
						param_add_message_to_Log( 'edited_user_birthday_month', T_('Please select a birthday month').'.', 'warning', T_('Validation errors:') );
					}
					else
					{
						param_check_number( 'edited_user_birthday_month', T_('Please select a birthday month').'.', $birthday_month_is_required );
					}
					// Check for valid values:
					if( isset( $edited_user_birthday_month ) )
					{
						param_integer_range( 'edited_user_birthday_month', 1, 12,
								sprintf( T_('The birthday month must be between %d and %d.'), 1, 12 ),
								! $is_api_request || $birthday_month_is_required ? true : NULL );

						$birthdate['month'] = $edited_user_birthday_month;
					}
					$this->set_from_Request( 'birthday_month', 'edited_user_birthday_month', true );
				}
			}

			if( $Settings->get( 'birthday_day' ) != 'hidden' )
			{	// Save Birthday Day
				$birthday_day_is_required = $Settings->get( 'birthday_day' ) == 'required';
				$edited_user_birthday_day = param( 'edited_user_birthday_day', 'integer', ! $is_api_request || $birthday_day_is_required ? true : NULL );
				if( isset( $edited_user_birthday_day ) || $is_identity_form )
				{
					if( $birthday_day_is_required && $has_moderate_access && empty( $edited_user_birthday_day ) )
					{
						param_add_message_to_Log( 'edited_user_birthday_day', T_('Please select a birthday day').'.', 'warning', T_('Validation errors:') );
					}
					else
					{
						param_check_number( 'edited_user_birthday_day', T_('Please select a birthday day').'.', $birthday_day_is_required );
					}
					// Chehck for valid values:
					if( isset( $edited_user_birthday_day ) )
					{
						param_integer_range( 'edited_user_birthday_day', 1, 31,
								sprintf( T_('The birthday day must be between %d and %d.'), 1, 31 ),
								! $is_api_request || $birthday_day_is_required ? true : NULL );

						$birthdate['day'] = $edited_user_birthday_day;
					}
					$this->set_from_Request( 'birthday_day', 'edited_user_birthday_day', true );
				}
			}

			if( $Settings->get( 'birthday_year' ) != 'hidden' )
			{	// Save Birthday Year
				$birthday_year_is_required = $Settings->get( 'birthday_year' ) == 'required';
				$edited_user_birthday_year = param( 'edited_user_birthday_year', 'integer', ! $is_api_request || $birthday_year_is_required ? true : NULL );
				if( isset( $edited_user_birthday_year ) || $is_identity_form )
				{
					if( $birthday_year_is_required && $has_moderate_access && empty( $edited_user_birthday_year ) )
					{
						param_add_message_to_Log( 'edited_user_birthday_year', T_('Please select a birthday year').'.', 'warning', T_('Validation errors:') );
					}
					else
					{
						param_check_number( 'edited_user_birthday_year', T_('Please select a birthday year').'.', $birthday_year_is_required );
					}
					// Check for valid values:
					if( isset( $edited_user_birthday_year ) )
					{
						param_integer_range( 'edited_user_birthday_year', 1900, (int) date( 'Y' ),
								sprintf( T_('The birthday year must be between %d and %d.'), 1900, (int) date( 'Y' ) ),
								! $is_api_request || $birthday_year_is_required ? true : NULL );

						$birthdate['year'] = $edited_user_birthday_year;
						if( $Settings->get( 'minimum_age' ) > 0 && ! empty( $edited_user_birthday_year ) )
						{ 	// We can only assume age if year is provided:
							if( empty( $birthdate['month']) )
							{	// Assume latest month:
								$birthdate['month'] = 12;
							}
							if( empty( $birthdate['day']) )
							{	// Assume latest day of the month-year:
								$temp_date = strtotime( $birthdate['year'].'-'.$birthdate['month'].'-01' );
								$birthdate['day'] = (int) date( 't', $temp_date );
							}

							$birth_date = new DateTime();
							$today     = new DateTime();
							$birth_date->setDate( $birthdate['year'], $birthdate['month'], $birthdate['day'] );
							$interval = date_diff( $today, $birth_date );
							$age = (int) $interval->format( '%y' );
							if( $age < $Settings->get( 'minimum_age' ) )
							{
								param_error( 'edited_user_birthday_year', sprintf( T_('You must be at least %d years old to use this service.'), $Settings->get( 'minimum_age' ) ) );
							}
						}
					}
					$this->set_from_Request( 'birthday_year', 'edited_user_birthday_year', true );
				}
			}

			$firstname_editing = $Settings->get( 'firstname_editing' );
			if( ( in_array( $firstname_editing, $edited_user_perms ) && $this->ID == $current_User->ID ) || ( $firstname_editing != 'hidden' && $has_moderate_access ) )
			{	// User has a permissions to save Firstname
				$edited_user_firstname = param( 'edited_user_firstname', 'string', ! $is_api_request || $firstname_editing == 'edited-user-required' ? true : NULL );
				if( isset( $edited_user_firstname ) || $is_identity_form )
				{
					if( $firstname_editing == 'edited-user-required' )
					{	// First name is required
						param_check_not_empty( 'edited_user_firstname', T_('Please enter your first name.') );
					}
					$this->set_from_Request('firstname', 'edited_user_firstname', true);
				}
			}

			$lastname_editing = $Settings->get( 'lastname_editing' );
			if( ( in_array( $lastname_editing, $edited_user_perms ) && $this->ID == $current_User->ID ) || ( $lastname_editing != 'hidden' && $has_moderate_access ) )
			{	// User has a permissions to save Lastname
				$edited_user_lastname = param( 'edited_user_lastname', 'string', ! $is_api_request || $lastname_editing == 'edited-user-required' ? true : NULL );
				if( isset( $edited_user_lastname ) || $is_identity_form )
				{
					if( $lastname_editing == 'edited-user-required' )
					{	// Last name is required
						param_check_not_empty( 'edited_user_lastname', T_('Please enter last name.') );
					}
					$this->set_from_Request('lastname', 'edited_user_lastname', true);
				}
			}

			$nickname_editing = $Settings->get( 'nickname_editing' );
			if( ( in_array( $nickname_editing, $edited_user_perms ) && $this->ID == $current_User->ID ) || ( $nickname_editing != 'hidden' && $has_moderate_access ) )
			{	// User has a permissions to save Nickname
				$edited_user_nickname = param( 'edited_user_nickname', 'string', ! $is_api_request || $nickname_editing == 'edited-user-required' ? true : NULL );
				if( isset( $edited_user_nickname ) || $is_identity_form )
				{
					if( $nickname_editing == 'edited-user-required' )
					{	// Nickname is required
						param_check_not_empty( 'edited_user_nickname', T_('Please enter your nickname.') );
					}
					$this->set_from_Request('nickname', 'edited_user_nickname', true);
				}
			}

			if( $this->ID == $current_User->ID || $has_moderate_access )
			{	// No user latitude setting:
				$edited_user_gender = param( 'edited_user_gender', 'string' );
				if( isset( $edited_user_gender ) || $is_identity_form )
				{
					$this->set_from_Request('gender', 'edited_user_gender', true);
				}
			}

			// ---- Locations / START ----
			load_funcs( 'regional/model/_regional.funcs.php' );

			if( user_country_visible() )
			{ // Save country
				$country_is_required = ( $Settings->get( 'location_country' ) == 'required' && countries_exist() );
				$edited_user_ctry_ID = param( 'edited_user_ctry_ID', 'integer', ! $is_api_request || $country_is_required ? true : NULL );
				if( isset( $edited_user_ctry_ID ) || $is_identity_form )
				{
					if( $country_is_required && $has_moderate_access && $edited_user_ctry_ID == 0 )
					{ // Display a note message if user can edit all users
						param_add_message_to_Log( 'edited_user_ctry_ID', T_('Please select a country').'.', 'note' );
					}
					else
					{ // Display an error message
						param_check_number( 'edited_user_ctry_ID', T_('Please select a country').'.', $country_is_required );
					}
					$this->set_from_Request( 'ctry_ID', 'edited_user_ctry_ID', true );
				}
			}

			if( user_region_visible() )
			{ // Save region
				$region_is_required = ( $Settings->get( 'location_region' ) == 'required' && regions_exist( $edited_user_ctry_ID ) );
				$edited_user_rgn_ID = param( 'edited_user_rgn_ID', 'integer', ! $is_api_request || $region_is_required ? true : NULL );
				if( isset( $edited_user_rgn_ID ) || $is_identity_form  )
				{
					if( $region_is_required && $has_moderate_access && $edited_user_rgn_ID == 0 )
					{ // Display a note message if user can edit all users
						param_add_message_to_Log( 'edited_user_rgn_ID', T_('Please select a region').'.', 'note' );
					}
					else
					{ // Display an error message
						param_check_number( 'edited_user_rgn_ID', T_('Please select a region'), $region_is_required );
					}
					$this->set_from_Request( 'rgn_ID', 'edited_user_rgn_ID', true );
				}
			}

			if( user_subregion_visible() )
			{ // Save subregion
				$subregion_is_required = ( $Settings->get( 'location_subregion' ) == 'required' && subregions_exist( $edited_user_rgn_ID ) );
				$edited_user_subrg_ID = param( 'edited_user_subrg_ID', 'integer', ! $is_api_request || $subregion_is_required ? true : NULL );
				if( isset( $edited_user_subrg_ID ) || $is_identity_form  )
				{
					if( $subregion_is_required && $has_moderate_access && $edited_user_subrg_ID == 0 )
					{ // Display a note message if user can edit all users
						param_add_message_to_Log( 'edited_user_subrg_ID', T_('Please select a sub-region').'.', 'note' );
					}
					else
					{ // Display an error message
						param_check_number( 'edited_user_subrg_ID', T_('Please select a sub-region').'.', $subregion_is_required );
					}
					$this->set_from_Request( 'subrg_ID', 'edited_user_subrg_ID', true );
				}
			}

			if( user_city_visible() )
			{ // Save city
				$city_is_required = ( $Settings->get( 'location_city' ) == 'required' && cities_exist( $edited_user_ctry_ID, $edited_user_rgn_ID, $edited_user_subrg_ID ) );
				$edited_user_city_ID = param( 'edited_user_city_ID', 'integer', ! $is_api_request || $city_is_required ? true : NULL );
				if( isset( $edited_user_city_ID ) || $is_identity_form )
				{
					if( $city_is_required && $has_moderate_access && $edited_user_city_ID == 0 )
					{ // Display a note message if user can edit all users
						param_add_message_to_Log( 'edited_user_city_ID', T_('Please select a city').'.', 'note' );
					}
					else
					{ // Display an error message
						param_check_number( 'edited_user_city_ID', T_('Please select a city').'.', $city_is_required );
					}
					$this->set_from_Request( 'city_ID', 'edited_user_city_ID', true );
				}
			}
			// ---- Locations / END ----


			// ---- Organizations / START ----
			$organizations = param( 'organizations', 'array:string' );
			$org_roles = param( 'org_roles', 'array:string' );
			$org_priorities = param( 'org_priorities', 'array:string' );
			$this->update_organizations( $organizations, $org_roles, $org_priorities );
			// ---- Organizations / END ----


			// ---- Additional Fields / START ----

			// Load all defined userfields for following checking of required fields
			$this->userfield_defs_load();

			// EXPERIMENTAL user fields & EXISTING fields:
			// Get indices of existing userfields:
			$userfield_IDs = $DB->get_results( '
						SELECT uf_ID, uf_ufdf_ID
							FROM T_users__fields
						 WHERE uf_user_ID = '.$this->ID );

			foreach( $userfield_IDs as $userfield )
			{
				if( ! isset( $this->userfield_defs[$userfield->uf_ufdf_ID] ) )
				{	// If user field definition doesn't exist in DB then delete field value of this user:
					$this->userfield_update( $userfield->uf_ID, NULL );
					continue;
				}

				if( ! userfield_is_viewable( $this->ID, $this->userfield_defs[$userfield->uf_ufdf_ID][5], $this->userfield_defs[$userfield->uf_ufdf_ID][0] ) )
				{	// Current user cannot update the user field:
					continue;
				}

				$uf_val = param( 'uf_'.$userfield->uf_ID, 'raw' );
				$field_type = $this->userfield_defs[$userfield->uf_ufdf_ID][0];
				if( $field_type == 'number' )
				{	// Change number type of integer because we have this type name preparing in function param_format():
					$field_type = 'integer';
				}
				elseif( $field_type != 'text' && $field_type != 'url' )
				{	// Use all other params as string, Only text and url have a preparing in function param_format():
					$field_type = 'string';
				}
				$uf_val = param_format( $uf_val, $field_type );

				if( $this->userfield_defs[$userfield->uf_ufdf_ID][0] == 'list' && $uf_val == '---' )
				{	// Option list has a value '---' for empty value
					$uf_val = '';
				}

				if( empty( $uf_val ) && $this->userfield_defs[$userfield->uf_ufdf_ID][2] == 'require' )
				{	// Display error for empty required field
					if( $has_full_access )
					{	// Display a note message if user can edit all users
						param_add_message_to_Log( 'uf_'.$userfield->uf_ID, sprintf( T_('Please enter a value for the field "%s".'), $this->userfield_defs[$userfield->uf_ufdf_ID][1] ), 'note' );
					}
					else
					{	// Display an error message
						param_error( 'uf_'.$userfield->uf_ID, T_('Please enter a value.') );
					}
				}
				else
				{	// Update field
					if( $this->userfield_defs[$userfield->uf_ufdf_ID][0] == 'url' )
					{	// Check url fields
						param_check_url( 'uf_'.$userfield->uf_ID, 'commenting' );
					}
					if( $this->userfield_defs[$userfield->uf_ufdf_ID][4] == 'list' )
					{	// Option "Multiple values" == "List style"
						// Split by comma and save each phrase as separate field
						$uf_val = explode( ',', $uf_val );
						foreach( $uf_val as $v => $val )
						{
							$val = trim( $val );
							if( $v == 0 )
							{	// Update field with first value
								$this->userfield_update( $userfield->uf_ID, $val );
							}
							else if( !empty( $val ) )
							{	// Add a new field for new values
								$this->userfield_add( $userfield->uf_ufdf_ID, $val );
							}
						}
					}
					else
					{	// Forbidden & Allowed fields
						$this->userfield_update( $userfield->uf_ID, $uf_val );
					}
				}
			}

			// Duplicate fields:
			if( $is_new_user )
			{
				$user_id = param( 'orig_user_ID', 'integer', 0 );
				if( $user_id !== 0 )
				{
					$SQL = new SQL( 'Get all user fields of duplicated user #'.$user_id );
					$SQL->SELECT( 'uf_ID, ufdf_ID, ufdf_type' );
					$SQL->FROM( 'T_users__fields' );
					$SQL->FROM_add( 'INNER JOIN T_users__fielddefs ON uf_ufdf_ID = ufdf_ID' );
					$SQL->WHERE( 'uf_user_ID = '.$user_id );
					$copied_userfields = $DB->get_results( $SQL );
					foreach( $copied_userfields as $copied_userfield )
					{
						$uf_val = param( 'uf_'.$copied_userfield->uf_ID, 'raw' );
						$field_type = $copied_userfield->ufdf_type;
						if( $field_type == 'number' )
						{	// Change number type of integer because we have this type name preparing in function param_format():
							$field_type = 'integer';
						}
						elseif( $field_type != 'text' && $field_type != 'url' )
						{	// Use all other params as string, Only text and url have a preparing in function param_format():
							$field_type = 'string';
						}
						$uf_val = param_format( $uf_val, $field_type );
						if( ! empty( $uf_val ) )
						{	// Copy user field only if it is not empty:
							$this->userfield_add( $copied_userfield->ufdf_ID, $uf_val );
						}
					}
				}
			}

			$uf_new_fields = param( 'uf_new', 'array' );	// Recommended & required fields (it still not saved in DB)
			$uf_add_fields = param( 'uf_add', 'array' );	// Added fields

			// Add a new field: (JS is not enabled)
			if( $action == 'add_field' )
			{	// Button 'Add' new field is pressed
				$new_field_type = param( 'new_field_type', 'integer', 0 );
				if( empty( $new_field_type ) )
				{	// We cannot add a new field without type
					param_error( 'new_field_type', T_('Please select a field type.') );
				}
				else
				{	// Save an adding field(in the array) to display it again if errors will be exist
					$new_field_type_exists = false;

					if( $this->userfield_defs[$new_field_type][4] == 'allowed' || $this->userfield_defs[$new_field_type][4] == 'list' )
					{	// This field can be duplicated
						global $add_field_types;
						$add_field_types = array( $new_field_type );
					}
					else
					{	// We should to solve we can add this field or don't
						if( ! isset( $uf_new_fields[$new_field_type] ) && ! isset( $uf_add_fields[$new_field_type] ) )
						{	// User is adding this field first time
							if( is_array( $userfield_IDs ) && count( $userfield_IDs ) > 0 )
							{	// User has fields that saved in DB
								foreach( $userfield_IDs as $userfield )
								{
									if( $userfield->uf_ufdf_ID == $new_field_type )
									{	// New adding field already exists for current user in DB
										$new_field_type_exists = true;
										break;
									}
								}
							}
							if( ! $new_field_type_exists )
							{	// Field doesn't still exist for current user
								global $add_field_types;
								$add_field_types = array( $new_field_type );
							}
						}
						else
						{	// Field exists, no duplicates available
							$new_field_type_exists = true;
						}

						if( $new_field_type_exists )
						{	// Field already is added for current user, we should display error about this
							param_error( 'new_field_type', T_('You already added this field, please select another.') );
						}
					}

					if( ! $new_field_type_exists )
					{	// Mark a new field to enter a value
						param_error( 'uf_add['.$new_field_type.'][]', T_('Please enter a value in this new field.') );
					}
				}
			}

			// Save a New recommended & require fields AND Adding fields
			if( count( $uf_new_fields ) > 0 || count( $uf_add_fields ) > 0 )
			{
				$uf_fields = array(
					'new' => $uf_new_fields,
					'add' => $uf_add_fields
				);
				foreach( $uf_fields as $uf_type => $uf_new_fields )
				{
					if( $uf_type == 'add' )
					{	// Save an adding fields to display it again if errors will be exist
						global $add_field_types;
						if( ! isset( $add_field_types ) )
						{	// Don't rewrite already existing array
							$add_field_types = array();
						}
					}
					foreach( $uf_new_fields as $uf_new_id => $uf_new_vals )
					{
						if( ! userfield_is_viewable( $this->ID, $this->userfield_defs[$uf_new_id][5], $this->userfield_defs[$uf_new_id][0] ) )
						{	// Current user cannot add the user field:
							continue;
						}

						foreach( $uf_new_vals as $uf_new_val )
						{
							if( $this->userfield_defs[$uf_new_id][0] == 'list' && $uf_new_val == '---' )
							{	// Option list has a value '---' for empty value
								$uf_new_val = '';
							}

							if( $uf_new_val != '' )
							{	// Insert a new field in DB if it is filled:
								switch( $this->userfield_defs[$uf_new_id][0] )
								{
									case 'url':
										// Format url field to valid value:
										$uf_new_val = param_format( $uf_new_val, 'url' );
										// Check url fields:
										param_check_url( 'uf_'.$uf_type.'['.$uf_new_id.'][]', 'commenting' );
										break;

									case 'text':
										// Format text field to valid value:
										$uf_new_val = param_format( $uf_new_val, 'text' );
										break;

									case 'number':
										// Format number field to valid value:
										$uf_new_val = param_format( $uf_new_val, 'integer' );
										break;

									case 'email':
									case 'word':
									case 'phone':
										// Format string fields to valid value:
										$uf_new_val = param_format( $uf_new_val, 'string' );
										break;
								}

								if( $this->userfield_defs[$uf_new_id][4] == 'list' )
								{	// Option "Multiple values" == "List style"
									// Split by comma and save each phrase as separate field
									$uf_new_val = explode( ',', $uf_new_val );
									foreach( $uf_new_val as $val )
									{
										$val = param_format( $val, 'string' );
										if( !empty( $val ) )
										{	// Exclude empty values(spaces)
											$this->userfield_add( (int)$uf_new_id, $val );
										}
									}
								}
								else
								{	// Forbidden & Allowed fields
									$this->userfield_add( (int)$uf_new_id, $uf_new_val );
								}
							}
							elseif( empty( $uf_new_val ) && $this->userfield_defs[$uf_new_id][2] == 'require' )
							{	// Display error for empty required field & new adding field
								if( $has_full_access )
								{	// Display a note message if user can edit all users
									param_add_message_to_Log( 'uf_'.$uf_type.'['.$uf_new_id.'][]', sprintf( T_('Please enter a value for the field "%s".'), $this->userfield_defs[$uf_new_id][1] ), 'note' );
								}
								else
								{	// Display an error message
									param_error( 'uf_'.$uf_type.'['.$uf_new_id.'][]', T_('Please enter a value.') );
								}
							}

							if( $uf_type == 'add' )
							{	// Save new added field, it used on the _user_identity.form
								$add_field_types[] = $uf_new_id;
							}
						}
					}
				}
			}
			// ---- Additional Fields / END ----

			// update profileupdate_date, because a publicly visible user property was changed
			$this->set_profileupdate_date();
		}

		// ******* Password form ******* //

		$is_password_form = param( 'password_form', 'boolean', false );
		if( $is_password_form || $is_new_user )
		{
			$reqID = param( 'reqID', 'string', '' );

			global $edited_user_pass1, $edited_user_pass2;

			$edited_user_pass1 = param( 'edited_user_pass1', 'string', true );
			// Remove the invalid chars from password var:
			$edited_user_pass1 = preg_replace( '/[<>&]/', '', $edited_user_pass1 );

			if( $request_password_confirmation )
			{	// Request a password confirmation:
				$edited_user_pass2 = param( 'edited_user_pass2', 'string', true );
				// Remove the invalid chars from password var:
				$edited_user_pass2 = preg_replace( '/[<>&]/', '', $edited_user_pass2 );
			}

			if( $is_new_user ||
			    ( ! empty( $reqID ) && $reqID == $Session->get( 'core.changepwd.request_id' ) ) ||
			    ( $this->get( 'pass_driver' ) == 'nopass' ) )
			{	// Current password is not required:
				//   - new user creating process
				//   - password change requested by email
				//   - password has not been set yet(email capture/quick registration)

				if( $is_new_user && param( 'init_pass', 'string' ) == 'user' )
				{	// Allow to create new user without password from back-office by admin:
					$this->set( 'pass_driver', 'nopass' );
					$this->set( 'pass', '' );
					$this->set( 'salt', '' );
				}
				elseif( $request_password_confirmation )
				{	// Request a password confirmation:
					if( param_check_passwords( 'edited_user_pass1', 'edited_user_pass2', true, $Settings->get('user_minpwdlen') ) )
					{ // We can set password
						$this->set_password( $edited_user_pass2 );
					}
				}
				else
				{	// Don't request a password confirmation and use only first entered password (Used on REST API):
					$this->set_password( $edited_user_pass1 );
				}
			}
			else
			{
				// ******* Password edit form ****** //

				$current_user_pass = param( 'current_user_pass', 'string', true );

				if( $this->ID != $current_User->ID )
				{ // Set the messages when admin changes a password of other user
					$checkpwd_params = array(
							'msg_pass_new'   => T_('Please enter new password.'),
							'msg_pass_twice' => T_('Please enter new password twice.'),
						);
				}
				else
				{ // Use default messages
					$checkpwd_params = array();
				}

				if( ! strlen( $current_user_pass ) )
				{
					param_error( 'current_user_pass' , T_('Please enter your current password.') );
					param_check_passwords( 'edited_user_pass1', 'edited_user_pass2', true, $Settings->get('user_minpwdlen'), $checkpwd_params );
				}
				else
				{
					if( $has_full_access && $this->ID != $current_User->ID )
					{	// Admin is changing a password of other user, Check a password of current admin
						$pass_User = $current_User;
					}
					else
					{	// User is changing own password
						$pass_User = $this;
					}
					if( $pass_User->check_password( $current_user_pass ) )
					{
						if( param_check_passwords( 'edited_user_pass1', 'edited_user_pass2', true, $Settings->get('user_minpwdlen'), $checkpwd_params ) )
						{ // We can set password
							$this->set_password( $edited_user_pass2 );
						}
					}
					else
					{
						param_error('current_user_pass' , T_('Your current password is incorrect.') );
						param_check_passwords( 'edited_user_pass1', 'edited_user_pass2', true, $Settings->get('user_minpwdlen'), $checkpwd_params );
					}
				}

			}

			if( $_POST['edited_user_pass1'] != trim( $_POST['edited_user_pass1'] ) ||
			    $_POST['edited_user_pass2'] != trim( $_POST['edited_user_pass2'] ) )
			{ // If new password was entered with spaces then inform user about this
				$Messages->add( T_('The leading and traling spaces have been trimmed from the new password.'), 'warning' );
			}
		}


		// Used in Preferences & Notifications forms
		$has_messaging_perm = $this->check_perm( 'perm_messaging', 'reply' );

		// ******* Preferences form ******* //

		$is_preferences_form = param( 'preferences_form', 'boolean', false );

		if( $is_preferences_form )
		{
			// Other preferences
			$preferred_locale = param( 'edited_user_locale', 'string', ! $is_api_request );
			if( isset( $preferred_locale ) )
			{
				$this->set_from_Request('locale', 'edited_user_locale', true);
			}

			// Session timeout
			$edited_user_timeout_sessions = param( 'edited_user_timeout_sessions', 'string', NULL );
			if( isset( $edited_user_timeout_sessions ) && ( $current_User->ID == $this->ID  || $has_full_access ) )
			{
				switch( $edited_user_timeout_sessions )
				{
					case 'default':
						$UserSettings->set( 'timeout_sessions', NULL, $this->ID );
						break;
					case 'custom':
						$UserSettings->set( 'timeout_sessions', param_duration( 'timeout_sessions' ), $this->ID );
						break;
				}
			}

			$edited_user_showonline = param( 'edited_user_showonline', 'integer', ! $is_api_request ? 0 : NULL );
			if( isset( $edited_user_showonline ) )
			{
				$UserSettings->set( 'show_online', $edited_user_showonline, $this->ID );
			}
		}

		// ******* Notifications form ******* //
		$is_subscriptions_form = param( 'subscriptions_form', 'boolean', false );
		$notifications_mode = $Settings->get( 'outbound_notifications_mode' );
		if( $is_subscriptions_form )
		{
			if( $action == 'subscribe' )
			{ // Do only subscribe to new blog (Don't update the user's settings from the same form)

				// If notifications are temporarily disabled, no subscription options should be lost when user saves.
				// When notification are turned back on, the subscription will resume as they were before.
				if( $notifications_mode != 'off' )
				{
					// A selected blog to subscribe
					$subscribe_blog_ID = param( 'subscribe_blog', 'integer', 0 );

					$sub_items    = 1;
					$sub_items_mod = 0;
					$sub_comments = 0; // We normally subscribe to new posts only

					// Note: we do not check if subscriptions are allowed here, but we check at the time we're about to send something
					if( $subscribe_blog_ID )
					{ // We need to record values:
						$BlogCache = & get_BlogCache();
						$subscribe_Blog = & $BlogCache->get_by_ID( $subscribe_blog_ID, false, false );

						if( $subscribe_Blog->has_access( $this ) )
						{ // user has access to the collection

							// erhsatingin > it seems we do need to check if subscription is allowed, at least for new posts
							if( ! $subscribe_Blog->get_setting( 'allow_subscriptions' ) )
							{ // Subscription to new posts is not allowed so we subscribe to new comments instead
								$sub_comments = 1;
							}

							$DB->query( 'REPLACE INTO T_subscriptions( sub_coll_ID, sub_user_ID, sub_items, sub_items_mod, sub_comments )
								VALUES ( '.$DB->quote( $subscribe_blog_ID ).', '.$DB->quote( $this->ID ).', '.$DB->quote( $sub_items ).', '.$DB->quote( $sub_items_mod ).', '.$DB->quote( $sub_comments ).' )' );

							$Messages->add( T_('Subscriptions have been changed.'), 'success' );
						}
						else
						{
							$Messages->add( T_('User has no access to the selected blog.'), 'error' );
						}
					}
					else
					{ // Display an error message to inform user about incorrect actions
						$Messages->add( T_('Please select at least one setting to subscribe on the selected blog.'), 'error' );
					}
				}
			}
			else
			{ // Update user's settings

				// Email communication
				$edited_user_email = param( 'edited_user_email', 'string', ! $is_api_request ? true : NULL );
				if( isset( $edited_user_email ) )
				{
					$edited_user_email = utf8_strtolower( $edited_user_email );
					param_check_not_empty( 'edited_user_email', T_('Please enter your e-mail address.') );
					param_check_email( 'edited_user_email', true );
					$this->set_email( $edited_user_email );
				}

				// set messaging options
				$enable_PM = param( 'enable_PM', 'integer', ! $is_api_request ? 0 : NULL );
				if( isset( $enable_PM ) && $has_messaging_perm )
				{
					$UserSettings->set( 'enable_PM', $enable_PM, $this->ID );
				}
				$emails_msgform = $Settings->get( 'emails_msgform' );
				if( ( $emails_msgform == 'userset' ) || ( ( $emails_msgform == 'adminset' ) && $has_full_access ) )
				{ // enable email option is displayed only if user can set or if admin can set and current User is an administrator
					$UserSettings->set( 'enable_email', param( 'enable_email', 'integer', 0 ), $this->ID );
				}

				// Email format
				$UserSettings->set( 'email_format', param( 'edited_user_email_format', 'string', 'auto' ), $this->ID );

				// set notification options
				if( $has_messaging_perm )
				{ // update 'notify messages' only if user has messaging rights and this option was displayed
					$UserSettings->set( 'notify_messages', param( 'edited_user_notify_messages', 'integer', 0 ), $this->ID );
					$UserSettings->set( 'notify_unread_messages', param( 'edited_user_notify_unread_messages', 'integer', 0 ), $this->ID );
				}
				$UserSettings->set( 'notify_comment_mentioned', param( 'edited_user_notify_comment_mentioned', 'integer', 0 ), $this->ID );
				if( $this->check_role( 'post_owner' ) )
				{ // update 'notify_published_comments' only if user has at least one post or user has right to create new post
					$UserSettings->set( 'notify_published_comments', param( 'edited_user_notify_publ_comments', 'integer', 0 ), $this->ID );
				}
				$is_comment_moderator = $this->check_role( 'comment_moderator' );
				if( $is_comment_moderator || $this->check_role( 'comment_editor' ) )
				{	// update 'notify_comment_moderation', 'notify_edit_cmt_moderation' and 'notify_spam_cmt_moderation' only if user is comment moderator/editor at least in one collection:
					$UserSettings->set( 'notify_comment_moderation', param( 'edited_user_notify_cmt_moderation', 'integer', 0 ), $this->ID );
					$UserSettings->set( 'notify_edit_cmt_moderation', param( 'edited_user_notify_edit_cmt_moderation', 'integer', 0 ), $this->ID );
					$UserSettings->set( 'notify_spam_cmt_moderation', param( 'edited_user_notify_spam_cmt_moderation', 'integer', 0 ), $this->ID );
				}
				if( $this->check_perm( 'admin', 'restricted', false ) )
				{	// update 'notify_meta_comment_mentioned' ans 'notify_meta_comments' only if edited user has a permission to back-office:
					$UserSettings->set( 'notify_meta_comment_mentioned', param( 'edited_user_notify_meta_comment_mentioned', 'integer', 0 ), $this->ID );
					$UserSettings->set( 'notify_meta_comments', param( 'edited_user_notify_meta_comments', 'integer', 0 ), $this->ID );
				}
				if( $is_comment_moderator )
				{ // update 'send_cmt_moderation_reminder' only if user is comment moderator at least in one blog
					$UserSettings->set( 'send_cmt_moderation_reminder', param( 'edited_user_send_cmt_moderation_reminder', 'integer', 0 ), $this->ID );
				}
				$UserSettings->set( 'notify_post_mentioned', param( 'edited_user_notify_post_mentioned', 'integer', 0 ), $this->ID );
				if( $this->check_role( 'post_moderator' ) )
				{	// update 'notify_post_moderation', 'notify_edit_pst_moderation' and 'send_cmt_moderation_reminder' only if user is post moderator at least in one collection:
					$UserSettings->set( 'notify_post_moderation', param( 'edited_user_notify_post_moderation', 'integer', 0 ), $this->ID );
					$UserSettings->set( 'notify_edit_pst_moderation', param( 'edited_user_notify_edit_pst_moderation', 'integer', 0 ), $this->ID );
					$UserSettings->set( 'notify_post_proposed', param( 'edited_user_notify_post_proposed', 'integer', 0 ), $this->ID );
					$UserSettings->set( 'send_pst_moderation_reminder', param( 'edited_user_send_pst_moderation_reminder', 'integer', 0 ), $this->ID );
					$UserSettings->set( 'send_pst_stale_alert', param( 'edited_user_send_pst_stale_alert', 'integer', 0 ), $this->ID );
				}
				if( $this->check_role( 'member' ) )
				{
					$UserSettings->set( 'notify_post_assignment', param( 'edited_user_notify_post_assignment', 'integer', 0 ), $this->ID );
				}
				if( $has_full_access )
				{
					$UserSettings->set( 'send_activation_reminder', param( 'edited_user_send_activation_reminder', 'integer', 0 ), $this->ID );
				}
				if( $Settings->get( 'inactive_account_reminder_threshold' ) > 0 )
				{	// If setting "Trigger after" of cron job "Send reminders about inactive accounts" is selected at least to 1 second:
					$UserSettings->set( 'send_inactive_reminder', param( 'edited_user_send_inactive_reminder', 'integer', 0 ), $this->ID );
				}

				if( $this->check_perm( 'users', 'edit' ) )
				{ // edited user has permission to edit all users, save notification preferences
					$UserSettings->set( 'notify_new_user_registration', param( 'edited_user_notify_new_user_registration', 'integer', 0 ), $this->ID );
					$UserSettings->set( 'notify_activated_account', param( 'edited_user_notify_activated_account', 'integer', 0 ), $this->ID );
					$UserSettings->set( 'notify_closed_account', param( 'edited_user_notify_closed_account', 'integer', 0 ), $this->ID );
					$UserSettings->set( 'notify_reported_account', param( 'edited_user_notify_reported_account', 'integer', 0 ), $this->ID );
					$UserSettings->set( 'notify_changed_account', param( 'edited_user_notify_changed_account', 'integer', 0 ), $this->ID );
				}

				if( $this->check_perm( 'options', 'edit' ) )
				{ // edited user has permission to edit options, save notification preferences
					$UserSettings->set( 'notify_cronjob_error', param( 'edited_user_notify_cronjob_error', 'integer', 0 ), $this->ID );
				}

				if( $has_full_access && $this->check_perm( 'options', 'view' ) )
				{	// current User is an administrator and the edited user has a permission to automations:
					$UserSettings->set( 'notify_automation_owner', param( 'edited_user_notify_automation_owner', 'integer', 0 ), $this->ID );
				}

				// Newsletters:
				$newsletter_subscriptions = param( 'edited_user_newsletters', 'array:integer', NULL );
				$this->set_newsletter_subscriptions( $newsletter_subscriptions );
				$UserSettings->set( 'notify_list_new_subscriber', param( 'edited_user_notify_list_new_subscriber', 'integer', 0 ), $this->ID );
				$UserSettings->set( 'notify_list_lost_subscriber', param( 'edited_user_notify_list_lost_subscriber', 'integer', 0 ), $this->ID );

				// Emails limit per day
				param_integer_range( 'edited_user_notification_email_limit', 0, 999, T_('Notificaiton email limit must be between %d and %d.'), ! $is_api_request );
				$UserSettings->set( 'notification_email_limit', param( 'edited_user_notification_email_limit', 'integer', 0 ), $this->ID );
				param_integer_range( 'edited_user_newsletter_limit', 0, 999, T_('List limit must be between %d and %d.'), ! $is_api_request );
				$UserSettings->set( 'newsletter_limit', param( 'edited_user_newsletter_limit', 'integer', 0 ), $this->ID );

				// If notifications are temporarily disabled, no subscription options should be lost when user saves.
				// When notification are turned back on, the subscription will resume as they were before.
				if( $notifications_mode != 'off' )
				{
					/**
					 * Update the subscriptions:
					 */
					$subs_blog_IDs = param( 'subs_blog_IDs', 'string', ! $is_api_request ? true : NULL );
					$subs_item_IDs = param( 'subs_item_IDs', 'string', ! $is_api_request ? true : NULL );

					// Work the blogs:
					$BlogCache = & get_BlogCache();
					$subscription_values = array();
					$opt_out_values = array();
					$unsubscribed = array();
					if( $subs_blog_IDs )
					{
						$subs_blog_IDs = explode( ',', $subs_blog_IDs );

						foreach( $subs_blog_IDs as $loop_blog_ID )
						{
							// Make sure no dirty hack is coming in here:
							$loop_blog_ID = intval( $loop_blog_ID );

							// Get checkbox values:
							$sub_items    = param( 'sub_items_'.$loop_blog_ID,    'integer', 0 );
							$sub_comments = param( 'sub_comments_'.$loop_blog_ID, 'integer', 0 );
							$sub_items_mod = param( 'sub_items_mod_'.$loop_blog_ID, 'integer', 0 );

							if( $sub_items || $sub_comments || $sub_items_mod )
							{	// We have a subscription for this blog
								$subscription_values[] = "( $loop_blog_ID, $this->ID, $sub_items, $sub_items_mod, $sub_comments )";
							}
							else
							{	// No subscription here:

								// Check if opt-out and user is a member
								$Blog = & $BlogCache->get_by_ID( $loop_blog_ID );

								if( $Blog->get( 'advanced_perms' )
										&& ( ( $Blog->get_setting( 'allow_subscriptions' ) && $Blog->get_setting( 'opt_out_subscription' ) ) ||
										     ( $Blog->get_setting( 'allow_item_mod_subscriptions' ) && $Blog->get_setting( 'opt_out_item_mod_subscription' ) ) ||
										     ( $Blog->get_setting( 'allow_comment_subscriptions' ) && $Blog->get_setting( 'opt_out_comment_subscription' ) ) )
										&& $this->check_perm( 'blog_ismember', 'view', false, $loop_blog_ID ) )
								{
									$opt_item = $Blog->get_setting( 'allow_subscriptions' ) && $Blog->get_setting( 'opt_out_subscription' ) ? $sub_items : 0;
									$opt_item_mod = $Blog->get_setting( 'allow_item_mod_subscriptions' ) && $Blog->get_setting( 'opt_out_item_mod_subscription' ) ? $sub_items : 0;
									$opt_comment = $Blog->get_setting( 'allow_comment_subscriptions' ) && $Blog->get_setting( 'opt_out_comment_subscription' ) ? $sub_comments : 0;
									$opt_out_values[] = "( $loop_blog_ID, $this->ID, $opt_item, $opt_item_mod, $opt_comment )";
								}
								else
								{
									$unsubscribed[] = $loop_blog_ID;
								}
							}
						}
					}

					// Note: we do not check if subscriptions are allowed here, but we check at the time we're about to send something
					if( count( $subscription_values ) )
					{	// We need to record values:
						$DB->query( 'REPLACE INTO T_subscriptions( sub_coll_ID, sub_user_ID, sub_items, sub_items_mod, sub_comments )
													VALUES '.implode( ', ', $subscription_values ) );
					}

					if( count( $opt_out_values ) )
					{
						$DB->query( 'REPLACE INTO T_subscriptions( sub_coll_ID, sub_user_ID, sub_items, sub_items_mod, sub_comments )
													VALUES '.implode( ', ', $opt_out_values ) );
					}

					if( count( $unsubscribed ) )
					{	// We need to make sure some values are cleared:
						$DB->query( 'DELETE FROM T_subscriptions
													WHERE sub_user_ID = '.$this->ID.'
														AND sub_coll_ID IN ('.implode( ', ', $unsubscribed ).')' );
					}

					// Individual post subscriptions
					if( !empty( $subs_item_IDs ) )
					{ // user was subscribed to at least one post update notification
						$ItemCache = & get_ItemCache();
						$subs_item_IDs = explode( ',', $subs_item_IDs );
						$opt_out_values = array();
						$unsubscribed = array();

						// get list of aggregated items:
						$sub_group_IDs = param( 'item_grp_sub', 'array:string' );
						$sub_group_IDs = implode( ',', $sub_group_IDs );
						$sub_group_IDs = explode( ',', $sub_group_IDs );

						foreach( $subs_item_IDs as $loop_item_ID )
						{
							$isub_comments = param( 'items_subs_'.$loop_item_ID, 'integer', 0 );

							if( !param( 'item_sub_'.$loop_item_ID, 'integer', 0 ) && !in_array( $loop_item_ID, $sub_group_IDs ) )
							{ // user wants to unsubscribe from this post notifications
								$Item = $ItemCache->get_by_ID( $loop_item_ID );
								$blog_ID = $Item->get_blog_ID();
								$Blog = $BlogCache->get_by_ID( $blog_ID );

								if( $Blog->get( 'advanced_perms' )
										&& $Blog->get_setting( 'allow_item_subscriptions' )
										&& $Blog->get_setting( 'opt_out_item_subscription' )
										&& $this->check_perm( 'blog_ismember', 'view', false, $blog_ID ) )
								{
									$opt_out_values[] = "( $loop_item_ID, $this->ID, $isub_comments )";
								}
								else
								{
									$unsubscribed[] = $loop_item_ID;
								}

							}
						}
						if( !empty( $opt_out_values ) )
						{
							$DB->query( 'REPLACE INTO T_items__subscriptions( isub_item_ID, isub_user_ID, isub_comments )
													VALUES '.implode( ', ', $opt_out_values ) );
						}

						if( !empty( $unsubscribed ) )
						{ // unsubscribe list is not empty, delete not wanted subscriptions
							$DB->query( 'DELETE FROM T_items__subscriptions
													WHERE isub_user_ID = '.$this->ID.'
														AND isub_item_ID IN ('.implode( ', ', $unsubscribed ).')' );
						}
					}
				}
			}
		}

		// ******* Advanced form ******* //
		$is_advanced_form = param( 'advanced_form', 'boolean', false );

		if( $is_advanced_form )
		{
			/*
			 * We currently support only one backoffice skin, so we don't need a system for selecting the backoffice skin.
			$UserSettings->set( 'admin_skin', param( 'edited_user_admin_skin', 'string' ), $this->ID );
			 */

			// Action icon params:
			param_integer_range( 'edited_user_action_icon_threshold', 1, 5, T_('The threshold must be between 1 and 5.') );
			$UserSettings->set( 'action_icon_threshold', param( 'edited_user_action_icon_threshold', 'integer', true ), $this->ID );

			param_integer_range( 'edited_user_action_word_threshold', 1, 5, T_('The threshold must be between 1 and 5.') );
			$UserSettings->set( 'action_word_threshold', param( 'edited_user_action_word_threshold', 'integer'), $this->ID );

			$UserSettings->set( 'display_icon_legend', param( 'edited_user_legend', 'integer', 0 ), $this->ID );

			// Set bozo validador activation
			$UserSettings->set( 'control_form_abortions', param( 'edited_user_bozo', 'integer', 0 ), $this->ID );

			// Focus on first
			$UserSettings->set( 'focus_on_first_input', param( 'edited_user_focusonfirst', 'integer', 0 ), $this->ID );

			// Results per page
			$edited_user_results_page_size = param( 'edited_user_results_page_size', 'integer', NULL );
			if( isset( $edited_user_results_page_size ) )
			{
				$UserSettings->set( 'results_per_page', $edited_user_results_page_size, $this->ID );
			}
		}

		if( $is_preferences_form || ( $is_identity_form && $is_new_user ) )
		{	// Multiple session
			$multiple_sessions = $Settings->get( 'multiple_sessions' );
			if( ( $multiple_sessions != 'adminset_default_no' && $multiple_sessions != 'adminset_default_yes' ) || $has_full_access )
			{
				$login_multiple_sessions = param( 'edited_user_set_login_multiple_sessions', 'integer' );
				if( ! $is_api_request || ( $is_api_request && isset( $login_multiple_sessions ) ) )
				{
					$UserSettings->set( 'login_multiple_sessions', param( 'edited_user_set_login_multiple_sessions', 'integer', 0 ), $this->ID );
				}
			}
		}

		return ! param_errors_detected();
	}


	/**
	 * Perform various checks on user login
	 * 
	 * @return string User login
	 */
	function check_login()
	{
		global $current_User;
		global $is_api_request;

		$has_full_access = check_user_perm( 'users', 'edit' );
		$has_moderate_access = $current_User->can_moderate_user( $this->ID );

		// TRUE when we create new user:
		$is_new_user = ( $this->ID == 0 );

		$edited_user_login = param( 'edited_user_login', 'string', NULL );
		if( ! $is_api_request || ( $is_api_request && ( isset( $edited_user_login ) || $is_new_user ) ) )
		{ // login specifically included in API request
			if( empty( $edited_user_login ) )
			{	// Empty login
				param_error( 'edited_user_login', T_('Please enter your login.') );
			}
			if( param_check_valid_login( 'edited_user_login' ) )
			{	// If login is valid
				global $reserved_logins;
				if( $edited_user_login != $this->get( 'login' ) &&
				    ! empty( $reserved_logins ) &&
				    in_array( $edited_user_login, $reserved_logins ) &&
				    ( ! is_logged_in() || ! $has_full_access ) )
				{	// If login has been changed and new entered login is reserved and current User cannot use this:
					param_error( 'edited_user_login', T_('You cannot use this login because it is reserved.') );
				}
			}
		}
		else
		{
			set_param( 'edited_user_login', $this->login );
			$edited_user_login = $this->login;
		}

		$UserCache = & get_UserCache();
		$UserLogin = $UserCache->get_by_login( $edited_user_login );
		if( $UserLogin && $UserLogin->ID != $this->ID )
		{	// The login is already registered
			$login_error_message = T_( 'This login already exists.' );
			if( $has_full_access )
			{
				$login_error_message = sprintf( T_( 'This login &laquo;%s&raquo; already exists. Do you want to <a %s>edit the existing user</a>?' ),
					$edited_user_login,
					'href="'.get_user_settings_url( 'profile', $UserLogin->ID ).'"' );
			}
			param_error( 'edited_user_login', $login_error_message );
		}

		if( !param_has_error( 'edited_user_login' ) )
		{	// We want all logins to be lowercase to guarantee uniqueness regardless of the database case handling for UNIQUE indexes:
			$this->set_from_Request( 'login', 'edited_user_login', true, 'utf8_strtolower' );
		}

		return $edited_user_login;
	}


	/**
	 * Get a param
	 *
	 * @param string the parameter
	 */
	function get( $parname )
	{
		if( $this->check_status( 'is_closed' ) && ( !is_admin_page() ) && ( $parname != 'login' ) && ( $parname != 'status' ) )
		{ // if this account is closed and we are not in backoffice, don't return other information then login or status
			return NULL;
		}

		switch( $parname )
		{
			case 'fullname':
				return trim($this->firstname.' '.$this->lastname);

			case 'preferredname':
				return $this->get_preferred_name();

			case 'num_posts':
				return $this->get_num_posts();

			case 'num_comments':
				return $this->get_num_comments();

			default:
			// All other params:
				return parent::get( $parname );
		}
	}


	/**
	 * Get the name of the account with complete details for admin select lists
	 *
	 * @return string
	 */
	function get_account_name()
	{
		if( $this->check_status( 'is_closed' ) && ( !is_admin_page() ) )
		{ // don't return closed accounts information except login name
			return $this->login;
		}

		return $this->login.' - '.$this->firstname.' '.$this->lastname.' ('.$this->nickname.')';
	}


	/**
	 * Get link to User
	 *
	 * @return string
	 */
	function get_link( $params = array() )
	{
		// Make sure we are not missing any param:
		$params = array_merge( array(
				'format'       => 'htmlbody',
				'link_to'      => 'userpage', // userurl userpage 'userurl>userpage'
				'link_text'    => 'preferredname', // avatar | preferredname
				'link_rel'     => '',
				'link_class'   => '',
				'thumb_size'   => 'crop-top-32x32',
				'thumb_class'  => '',
				'thumb_zoom'   => false,
				'thumb_overlay'=> '',
				'tag_size'     => NULL,
			), $params );

		if( $params['link_text'] == 'avatar' )
		{
			$r = $this->get_avatar_imgtag( $params['thumb_size'], $params['thumb_class'], '', $params['thumb_zoom'], $params['thumb_overlay'], '', $params['tag_size'] );
		}
		else
		{
			$r = $this->dget( 'preferredname', $params['format'] );
		}

		switch( $params['link_to'] )
		{
			case 'userpage':
			case 'userpage>userurl':
				$url = $this->get_userpage_url();
				break;

			case 'userurl':
				$url = $this->url;
				break;

			case 'userurl>userpage':
				// We give priority to user submitted url:
				if( utf8_strlen($this->url) > 10 )
				{
					$url = $this->url;
				}
				else
				{
					$url = $this->get_userpage_url();
				}
				break;
		}

		if( !empty($url) )
		{
			$link = '<a href="'.$url.'"';
			if( !empty($params['link_rel']) )
			{
				$link .= ' rel="'.$params['link_rel'].'"';
			}
			if( !empty($params['link_class']) )
			{
				$link .= ' class="'.$params['link_class'].'"';
			}
			$r = $link.'>'.$r.'</a>';
		}

		return $r;
	}


	/**
	 * Get preferred name of the user
	 *
	 * @return string
	 */
	function get_preferred_name()
	{
		if( $this->check_status( 'is_closed' ) && ( !is_admin_page() ) )
		{ // don't return closed accounts information except login name
			return $this->login;
		}

		if( !empty( $this->nickname ) )
		{	// Nickname
			return $this->get( 'nickname' );
		}
		elseif( !empty( $this->firstname ) || !empty( $this->lastname ) )
		{	// First name
			return $this->get('fullname');
		}
		else
		{	// Login
			return $this->get( 'login' );
		}
	}


	/**
	 * Get user's login with gender color
	 *
	 * @param array Params
	 * @return string User's preferred name with gender color if this available
	 */
	function get_colored_login( $params = array() )
	{
		// Make sure we are not missing any param:
		$params = array_merge( array(
				'mask'         => '$login$', // $avatar$ $login$
				'login_format' => 'htmlbody',
				'avatar_size'  => 'crop-top-15x15',
				'login_text'   => 'login', // name | login
				'use_style'    => false, // true - to use attr "style", e.g. on email templates
				'extra_class'  => '',
				'protocol'     => '', // Protocol is used for gravatar, example: 'http:' or 'https:'
			), $params );

		$avatar = '';
		$login = '';
		$class = '';

		if( strpos( $params['mask'], '$login$' ) !== false )
		{ // Display login or preferred name
			if( $params['login_text'] == 'name' )
			{ // Use nickname or fullname
				$login = $this->get_username( $params['login_format'] );
			}
			else
			{ // Use a login
				$login = $this->dget( 'login', $params['login_format'] );
			}

			// Add class "login" to detect logins by js plugins
			$class = ( $login == $this->login ? 'login ' : '' );
		}

		if( strpos( $params['mask'], '$avatar$' ) !== false )
		{ // Display avatar:
			$avatar = $this->get_avatar_imgtag( $params['avatar_size'], '', '', false, '', '', NULL, $params['protocol'] );
		}

		$mask = array( '$login$', '$avatar$' );
		$data = array( $login, $avatar );

		$gender_class = $this->get_gender_class();
		$extra_class = '';
		if( !empty( $params['extra_class'] ) )
		{
			if( !is_array( $params['extra_class']))
			{
				$params['extra_class'] = array($params['extra_class']);
			}
			$extra_class = ' '.implode( ' ', $params['extra_class'] );
		}
		$attr_style = '';
		if( $params['use_style'] )
		{ // Use "style"
			$attr_style = emailskin_style( '.user+.'.str_replace( ' ', '.', $gender_class ).str_replace( ' ', '+.user.', $extra_class ) );
		}

		return '<span class="'.trim( $class.' '.$gender_class.$extra_class ).'"'.$attr_style.'>'.str_replace( $mask, $data, $params['mask'] ).'</span>';
	}


	/**
	 * Get User identity link, which is a composite of user avatar and login, both point to the specific user profile tab.
	 *
	 * @return string User avatar and login if the identity link is not available, the identity link otherwise.
	 */
	function get_identity_link( $params = array() )
	{
		// Make sure we are not missing any param:
		$params = array_merge( array(
				'link_text'      => 'avatar_name', // auto| avatar_name | avatar_login | only_avatar | name | login | nickname | firstname | lastname | fullname | preferredname
				'link_class'     => '', // Class for <a href=""
				'thumb_size'     => 'crop-top-15x15',
				'thumb_class'    => 'avatar_before_login',
				'thumb_zoomable' => false,
				'login_mask'     => '', // example: 'text $login$ text'  TODO: replace with template
				'login_class'    => 'identity_link_username',  // Not used if login_mask is used
				'display_bubbletip' => true,
				'nowrap'         => true,
				'user_tab'       => 'profile',
				'use_style'      => false, // true - to use attr "style" instead of "class", e.g. on email templates
				'blog_ID'        => NULL,
				'protocol'       => '', // Protocol is used for gravatar, example: 'http:' or 'https:'
			), $params );

		$identity_url = get_user_identity_url( $this->ID, $params['user_tab'], $params['blog_ID'] );

		$attr_bubbletip = '';
		if( $params['display_bubbletip'] )
		{	// Set attribute to initialize a bubbletip
			$attr_bubbletip = ' rel="bubbletip_user_'.$this->ID.'"';
		}

		$avatar_tag = '';
		if( strpos( $params['link_text'], 'avatar' ) !== false )
		{ // Avatar must be displayed in this link
			$avatar_tag = $this->get_avatar_imgtag( $params['thumb_size'], $params['thumb_class'], '', $params['thumb_zoomable'], '', '', NULL, $params['protocol'] );
			if( $params['thumb_zoomable'] )
			{ // User avatar is zoomable
				// Add tag param to init bubbletip
				$avatar_tag = str_replace( '<img ', '<img rel="bubbletip_user_'.$this->ID.'"', $avatar_tag );
				return $avatar_tag; // We should exit here, to avoid the double adding of tag <a>
			}
		}

		$linktext = '';
		$class = $params['link_class'];

		if( $params['link_text'] != 'only_avatar' )
		{ // Display a login, nickname, firstname, lastname, fullname or preferredname
			switch( $params['link_text'] )
			{
				case 'auto':		// TODO this should be the real auto
				case 'login':
				case 'avatar_login': 
					$linktext = $this->get_username();  // TODO: auto has been hardcoded into this ! :(
					break;
				case 'force_login':		// TODO: we should NOT need a "force" if we correctly use auto the rest of the time!
				case 'avatar_force_login':
					$linktext = $this->get( 'login' );
					break;
				case 'nickname':
					$linktext = $this->nickname;
					break;
				case 'firstname':
					$linktext = $this->firstname;
					break;
				case 'lastname':
					$linktext = $this->lastname;
					break;
				case 'fullname':
					$linktext = $this->firstname.' '.$this->lastname;
					break;
				case 'preferredname':
					$linktext = $this->get_preferred_name();
					break;
				// default: 'avatar_name' | 'avatar' | 'name'
			}
			$linktext = trim( $linktext );
			if( empty( $linktext ) )
			{ // Use a login or preferred name by default
				$linktext = $this->get_username();
			}

			// Add class "login" to detect logins by js plugins
			$class .= ( $linktext == $this->login ? ' login' : '' );
			
			if( !empty($params['login_mask']) )
			{ // Apply login mask
				$linktext = str_replace( '$login$', $linktext, $params['login_mask'] );
			}
			elseif( !empty($params['login_class']) )
			{
				$linktext = '<span class="'.$params['login_class'].'">'.$linktext.'</span>';
			}
		} // END Not for "only avatar"

		$gender_class = $this->get_gender_class();
		$attr_style = '';
		if( $params['use_style'] )
		{ // Use "style" (e-g email template)
			$attr_style = emailskin_style( '.user+.'.str_replace( ' ', '.', $gender_class ) );
		}
		$attr_style = ' class="'.trim( $class.' '.$gender_class.( $params['nowrap'] ? ' nowrap' : '' ) ).'"'.$attr_style;

		if( empty( $identity_url ) )
		{
			return '<span'.$attr_style.$attr_bubbletip.'>'.$avatar_tag.$linktext.'</span>';
		}

		$link_title = T_( 'Show the user profile' );
		return '<a href="'.$identity_url.'" title="'.$link_title.'"'.$attr_style.$attr_bubbletip.'>'.$avatar_tag.$linktext.'</a>';
	}


	/**
	 * Get Regional object
	 *
	 * @param string Object name (Country, Region, Subregion, City)
	 * @param string ID name
	 * @return object
	 */
	function & get_Regional( $Object, $ID )
	{
		if( $this->check_status( 'is_closed' ) && ( !is_admin_page() ) )
		{ // don't return closed accounts regional information to front office
			$r = false;
			return $r;
		}

		if( is_null($this->$Object) && !empty($this->$ID ) )
		{
			$Cache = call_user_func( 'get_'.$Object.'Cache' );
			$this->$Object = & $Cache->get_by_ID( $this->$ID, false );
		}

		return $this->$Object;
	}


	/**
	 * Get Regional name
	 *
	 * @param string Object name (Country, Region, Subregion, City)
	 * @param string ID name
	 * @return string Name of Country, Region, Subregion or City
	 */
	function get_regional_name( $Object, $ID )
	{
		if( $this->get_Regional( $Object, $ID ) )
		{	// We have an regional object:
			return $this->$Object->name;
		}

		return '';
	}


	/**
	 * Get Country object
	 */
	function & get_Country()
	{
		return $this->get_Regional( 'Country', 'ctry_ID' );
	}


	/**
	 * Get country name
	 */
	function get_country_name()
	{
		if( empty( $this->ctry_ID ) )
		{
			return;
		}

		load_class( 'regional/model/_country.class.php', 'Country' );
		return $this->get_regional_name( 'Country', 'ctry_ID' );
	}

	/**
	 * Get region name
	 */
	function get_region_name()
	{
		if( empty( $this->rgn_ID ) )
		{
			return;
		}

		load_class( 'regional/model/_region.class.php', 'Region' );
		return $this->get_regional_name( 'Region', 'rgn_ID' );
	}

	/**
	 * Get subregion name
	 */
	function get_subregion_name()
	{
		if( empty( $this->subrg_ID ) )
		{
			return;
		}

		load_class( 'regional/model/_subregion.class.php', 'Subregion' );
		return $this->get_regional_name( 'Subregion', 'subrg_ID' );
	}

	/**
	 * Get city name
	 *
	 * @param boolean TRUE - show postcode
	 * @return string city name
	 */
	function get_city_name( $show_postcode = true )
	{
		if( empty( $this->city_ID ) )
		{
			return '';
		}

		load_class( 'regional/model/_city.class.php', 'City' );
		$city = $this->get_regional_name( 'City', 'city_ID' );
		if( $this->City && $show_postcode )
		{	// Get postcode
			$city .= ' ('.$this->City->postcode.')';
		}
		return $city;
	}


	/**
	 * Get the number of blogs owned by this user
	 *
	 * @return integer
	 */
	function get_num_blogs()
	{
		global $DB;

		return $DB->get_var( 'SELECT count(*)
								FROM T_blogs
								WHERE blog_owner_user_ID = '.$this->ID );
	}


	/**
	 * Get the number of posts for the user.
	 *
	 * @param string Posts status
	 * @return integer
	 */
	function get_num_posts( $status = '' )
	{
		global $DB;
		global $collections_Module;

		if( isset( $collections_Module ) && is_null( $this->_num_posts ) )
		{
			$SQL = new SQL( 'Get the number of posts for the user' );
			$SQL->SELECT( 'post_status, COUNT(*)' );
			$SQL->FROM( 'T_items__item' );
			$SQL->WHERE( 'post_creator_user_ID = '.$this->ID );
			$SQL->GROUP_BY( 'post_status' );
			$this->_num_posts = $DB->get_assoc( $SQL->get() );

			// Calc number of posts with all statuses
			$total_num_posts = 0;
			foreach( $this->_num_posts as $status_num_posts )
			{
				$total_num_posts += $status_num_posts;
			}
			$this->_num_posts[''] = $total_num_posts;
		}

		return !empty( $this->_num_posts[ $status ] ) ? $this->_num_posts[ $status ] : 0;
	}


	/**
	 * Get the number of comments for the user.
	 *
	 * @param string Comments status
	 * @param boolean Count already recycled comments
	 * @return integer
	 */
	function get_num_comments( $status = '', $count_recycled = false )
	{
		global $DB;
		global $collections_Module;

		if( isset( $collections_Module ) && is_null( $this->_num_comments ) )
		{
			$SQL = new SQL( 'Get the number of comments for the user' );
			$SQL->SELECT( 'comment_status, COUNT(*)' );
			$SQL->FROM( 'T_comments' );
			$SQL->WHERE( 'comment_author_user_ID = '.$this->ID );
			$SQL->WHERE_and( 'comment_type IN ( "comment", "trackback", "pingback", "webmentions" )' );
			$SQL->GROUP_BY( 'comment_status' );
			$this->_num_comments = $DB->get_assoc( $SQL );

			// Calc number of comments with all statuses
			$total_num_comments = 0;
			foreach( $this->_num_comments as $curr_status => $status_num_comments )
			{
				if( $count_recycled || ( $curr_status != 'trash' ) )
				{
					$total_num_comments += $status_num_comments;
				}
			}
			$this->_num_comments[''] = $total_num_comments;
		}

		return !empty( $this->_num_comments[ $status ] ) ? $this->_num_comments[ $status ] : 0;
	}


	/**
	 * Get the number of files for the user.
	 *
	 * @param string File type: 'image', 'audio', 'other'
	 * @return integer
	 */
	function get_num_files( $type = NULL )
	{
		global $DB;

		if( is_null( $this->_num_files ) )
		{
			$files_SQL = new SQL();
			$files_SQL->SELECT( 'file_type, COUNT( file_ID ) AS cnt' );
			$files_SQL->FROM( 'T_files' );
			$files_SQL->WHERE( 'file_creator_user_ID = '.$this->ID );
			$files_SQL->GROUP_BY( 'file_type' );
			$this->_num_files = $DB->get_assoc( $files_SQL->get() );
		}

		return ! empty( $this->_num_files[ $type ] ) ? $this->_num_files[ $type ] : 0;
	}


	/*
	 * Get the total size of files uploaded by the user
	 *
	 * @param string File type: 'image', 'video', 'audio', 'other'; NULL - for all file types
	 * @return integer total size in bytes
	 */
	function get_total_upload( $type = NULL )
	{
		if( empty( $this->ID ) )
		{	// User must be saved in DB:
			return 0;
		}

		$FileCache = & get_FileCache();
		$FileCache->clear();
		$files_sql_where = 'file_creator_user_ID = '.$this->ID;
		if( $type !== NULL )
		{	// Restrict files by type:
			global $DB;
			$files_sql_where .= ' AND file_type = '.$DB->quote( $type );
		}
		$FileCache->load_where( $files_sql_where );

		$total_upload_size = 0;
		foreach( $FileCache->cache as $user_File )
		{
			$total_upload_size += $user_File->get_size();
		}

		return $total_upload_size;
	}


	/**
	 * Get the number of user sessions
	 *
	 * @param boolean set true to return the number of sessions as a link to the user sessions list
	 * @return integer|string number of sessions or link to user sessions where the link text is the number of sessions
	 */
	function get_num_sessions( $link_sessions = false )
	{
		global $DB;

		$num_sessions = $DB->get_var( 'SELECT count( sess_ID )
											FROM T_sessions
											WHERE sess_user_ID = '.$this->ID );

		if( $link_sessions && ( $num_sessions > 0 ) )
		{
			return $num_sessions.' - <a href="?ctrl=user&amp;user_ID='.$this->ID.'&amp;user_tab=sessions" class="'.button_class().' middle" title="'.format_to_output( T_('Go to user activity'), 'htmlattr' ).'">'.get_icon( 'magnifier', 'imgtag', array( 'title' => T_('Go to user activity') ) ).'</a>';
		}

		return $num_sessions;
	}


	/**
	 * Get the number of user messages
	 *
	 * @param string Type ( sent | received )
	 * @return integer the number of requested type of messages
	 */
	function get_num_messages( $type = 'sent' )
	{
		global $DB;

		if( $type == 'received' )
		{	// Get a count of messages received
			$SQL = new SQL( 'Get a count of messages received' );
			$SQL->SELECT( 'COUNT( msg_ID )' );
			$SQL->FROM( 'T_messaging__threadstatus' );
			$SQL->FROM_add( 'LEFT JOIN T_messaging__message ON tsta_thread_ID = msg_thread_ID' );
			$SQL->WHERE( 'tsta_user_ID = '.$DB->quote( $this->ID ) );
			$SQL->WHERE_and( 'msg_author_user_ID != '.$DB->quote( $this->ID ) );
		}
		else
		{	// Get a count of messages sent
			$SQL = new SQL( 'Get a count of messages sent' );
			$SQL->SELECT( 'COUNT( msg_ID )' );
			$SQL->FROM( 'T_messaging__message' );
			$SQL->WHERE( 'msg_author_user_ID = '.$DB->quote( $this->ID ) );
		}

		return $DB->get_var( $SQL );
	}


	/**
	 * Get the number of other users posts which were edited by this user
	 *
	 * @return integer the number of edited posts
	 */
	function get_num_edited_posts()
	{
		global $DB;

		return $DB->get_var( 'SELECT COUNT( DISTINCT( post_ID ) )
									FROM T_items__item
									INNER JOIN T_items__version ON post_ID = iver_itm_ID
									WHERE post_creator_user_ID <> '.$this->ID.' AND
										( iver_edit_user_ID = '.$this->ID.' OR post_lastedit_user_ID = '.$this->ID.' )' );
	}


	/**
	 * Get the number of polls owned by this user
	 *
	 * @return integer the number of owned polls
	 */
	function get_num_polls()
	{
		global $DB;

		if( empty( $this->ID ) )
		{
			return 0;
		}

		$SQL = new SQL( 'Get polls owned by user #'.$this->ID );
		$SQL->SELECT( 'COUNT( pqst_ID )' );
		$SQL->FROM( 'T_polls__question' );
		$SQL->WHERE( 'pqst_owner_user_ID = '.$this->ID );

		return $DB->get_var( $SQL );
	}


	/**
	 * Get a number of emails sent to this User
	 *
	 * @return integer
	 */
	function get_num_sent_emails()
	{
		global $DB;

		if( empty( $this->ID ) )
		{
			return 0;
		}

		$SQL = new SQL( 'Get a number of emails sent to the User #'.$this->ID );
		$SQL->SELECT( 'COUNT( emlog_ID )' );
		$SQL->FROM( 'T_email__log' );
		$SQL->WHERE( 'emlog_user_ID = '.$this->ID );

		return intval( $DB->get_var( $SQL ) );
	}


	/**
	 * Delete all emails sent to the user
	 *
	 * @return boolean True on success
	 */
	function delete_sent_emails()
	{
		global $DB;

		return $DB->query( 'DELETE
			 FROM T_email__log
			WHERE emlog_user_ID = '.$this->ID );
	}


	/**
	 * Get a number of email returns from this User's email address
	 *
	 * @return integer
	 */
	function get_num_email_returns()
	{
		global $DB;

		if( empty( $this->ID ) )
		{
			return 0;
		}

		$SQL = new SQL( 'Get a number of email returns from email address of the User #'.$this->ID );
		$SQL->SELECT( 'COUNT( emret_ID )' );
		$SQL->FROM( 'T_email__returns' );
		$SQL->WHERE( 'emret_address = '.$DB->quote( $this->get( 'email' ) ) );

		return intval( $DB->get_var( $SQL ) );
	}


	/**
	 * Delete all email returns from the user's email address
	 *
	 * @return boolean True on success
	 */
	function delete_email_returns()
	{
		global $DB;

		return $DB->query( 'DELETE
			 FROM T_email__returns
			WHERE emret_address = '.$DB->quote( $this->get( 'email' ) ) );
	}


	/**
	 * Get the path to the media directory. If it does not exist, it will be created.
	 *
	 * If we're {@link is_admin_page() on an admin page}, it adds status messages.
	 * @todo These status messages should rather go to a "syslog" and not be displayed to a normal user
	 * @todo dh> refactor this into e.g. create_media_dir() and use it for Blog::get_media_dir, too.
	 *
	 * @param boolean Create the directory, if it does not exist yet?
	 * @return mixed the path as string on success, false if the dir could not be created
	 */
	function get_media_dir( $create = true )
	{
		global $media_path, $Messages, $Settings, $Debuglog;

		if( ! $Settings->get( 'fm_enable_roots_user' ) )
		{	// User directories are disabled:
			$Debuglog->add( 'Attempt to access user media dir, but this feature is disabled', 'files' );
			return false;
		}

		$userdir = get_canonical_path( $media_path.$this->get_media_subpath() );

		if( $create && ! is_dir( $userdir ) )
		{
			if( ! is_writable( dirname($userdir) ) )
			{ // add error
				if( is_admin_page() )
				{
					$Messages->add( sprintf( T_("The user's media directory &laquo;%s&raquo; could not be created, because the parent directory is not writable or does not exist."), rel_path_to_base($userdir) )
							.get_manual_link('directory-creation-error'), 'error' );
				}
				return false;
			}
			elseif( ! evo_mkdir( $userdir ) )
			{ // add error
				if( is_admin_page() )
				{
					$Messages->add( sprintf( T_("The user's media directory &laquo;%s&raquo; could not be created."), rel_path_to_base($userdir) )
							.get_manual_link('directory-creation-error'), 'error' );
				}
				return false;
			}
			else
			{ // add note:
				if( is_admin_page() )
				{
					$Messages->add( sprintf( T_("The user's directory &laquo;%s&raquo; has been created with permissions %s."), rel_path_to_base($userdir), substr( sprintf('%o', fileperms($userdir)), -3 ) ), 'success' );
				}
			}
		}
		return $userdir;
	}


	/**
	 * Get the URL to the media folder
	 *
	 * @return string the URL
	 */
	function get_media_url()
	{
		global $media_url, $Settings, $Debuglog;
		global $Collection, $Blog;

		if( ! $Settings->get( 'fm_enable_roots_user' ) )
		{	// User directories are disabled:
			$Debuglog->add( 'Attempt to access user media URL, but this feature is disabled', 'files' );
			return false;
		}

		if( ! empty( $Blog ) && ! is_admin_page() )
		{	// We are currently looking at a collection on front-office.
			// We are going to consider (for now) that we want the users and their files
			// to appear as being part of that collection.
			return $Blog->get_local_media_url().$this->get_media_subpath();
		}

		// System media url:
		return $media_url.$this->get_media_subpath();
	}


	/**
	 * Get user media directory subpath, e.g. users/{login}/ or users/usr_{user ID}/
	 */
	function get_media_subpath()
	{
		if( is_valid_login( $this->login, true ) === true )
		{	// Valid ASCII login, use it as is
			return 'users/'.$this->login.'/';
		}
		else
		{	// Non-ASCII login
			return 'users/usr_'.$this->ID.'/';
		}
	}


	/**
	 * Get message form url
	 */
	function get_msgform_url( $formurl, $redirect_to = NULL )
	{
		global $ReqURI;

		if( ! $this->get_msgform_possibility() )
		{
			return NULL;
		}

		if( $redirect_to == NULL )
		{
			$redirect_to = $ReqURI;
		}

		return url_add_param( $formurl, 'recipient_id='.$this->ID.'&amp;redirect_to='.rawurlencode( $redirect_to ) );
	}


	/**
	 * Get user page url
	 *
	 * @param integer|NULL Blog ID or NULL to use current blog
	 * @param boolean TRUE to force use URL even when current user in not logged in
	 * @return string
	 */
	function get_userpage_url( $blog_ID = NULL, $force_url = false )
	{
		global $Settings;

		if( ! empty( $blog_ID ) )
		{ // Use blog from param by ID
			$BlogCache = & get_BlogCache();
			$current_Blog = & $BlogCache->get_by_ID( $blog_ID, false, false );
		}

		if( empty( $current_Blog ) )
		{ // Use current blog
			global $Collection, $Blog;
			$current_Blog = & $Blog;
		}

		if( empty( $current_Blog ) || empty( $Settings ) )
		{ // Wrong request
			return NULL;
		}

		if( $force_url || is_logged_in() )
		{ // If current User is logged in
			if( $Settings->get( 'user_url_loggedin' ) == 'url' && $this->get_field_url( true ) != '' )
			{ // Use website url if it is defined and setting enables this
				return $this->get_field_url( true );
			}
			else
			{ // Use an user page if url is not defined
				return $current_Blog->get( 'userurl', array( 'user_ID' => $this->ID, 'user_login' => $this->login ) );
			}
		}
		else
		{ // For anonymous users
			$this->get_Group();
			if( $this->Group->level >= $Settings->get( 'allow_anonymous_user_level_min' ) &&
			    $this->Group->level <= $Settings->get( 'allow_anonymous_user_level_max' ) )
			{ // Check if anonymous users have an access to view this user
				if( $Settings->get( 'user_url_anonymous' ) == 'url' && $this->get_field_url( true ) != '' )
				{ // Use website url if it is defined and setting enables this
					return $this->get_field_url( true );
				}
				elseif( $Settings->get( 'allow_anonymous_user_profiles' ) )
				{ // Use an user page if url is not defined and this is enabled by setting for anonymous users
					return $current_Blog->get( 'userurl', array( 'user_ID' => $this->ID, 'user_login' => $this->login ) );
				}
			}
		}

		// return NULL if user page url is not allowed
		return NULL;
	}

	/**
	 * Get url visits url
	 *
	 * @param integer Collection ID
	 * @return string
	 */
	function get_visits_url( $blog_ID = NULL )
	{
		if( ! empty( $blog_ID ) )
		{	// Use collection from param by ID:
			$BlogCache = & get_BlogCache();
			$url_Blog = & $BlogCache->get_by_ID( $blog_ID, false, false );
		}

		if( empty( $url_Blog ) )
		{	// Use current collection:
			global $Collection, $Blog;
			$url_Blog = & $Blog;
		}

		if( empty( $url_Blog ) )
		{	// Wrong request:
			return NULL;
		}

		return url_add_param( $url_Blog->get( 'visitsurl' ), 'user_ID='.$this->ID );
	}


	/**
	 * Get url from defined userfields
	 *
	 * @param boolean TRUE if we want get a value from url fields of the table T_users__fields, FALSE - get from $this->url
	 * @return string Url
	 */
	function get_field_url( $from_extra_fields = false )
	{
		global $DB;

		if( $from_extra_fields )
		{	// Get value from DB or from cache
			if( isset( $this->field_url ) )
			{	// Get url from variable already defined in first calling of this method
				return $this->field_url;
			}
			else
			{	// Get value from DB
				$this->field_url = (string)$DB->get_var( '
					SELECT uf_varchar
						FROM T_users__fields
							LEFT JOIN T_users__fielddefs ON uf_ufdf_ID = ufdf_ID
							LEFT JOIN T_users__fieldgroups ON ufdf_ufgp_ID = ufgp_ID
					WHERE uf_user_ID = '.$this->ID.'
						AND ufdf_type = "url"
					ORDER BY ufgp_order, ufdf_order, uf_ID
					LIMIT 1' );
				return $this->field_url;
			}
		}
		else
		{	// Get value from $this->url (T_users)
			return $this->url;
		}
	}


	/**
	 * Get link from defined userfields
	 *
	 * @param array Template params
	 * @return string Link
	 */
	function get_field_link( $params = array() )
	{
		$params = array_merge( array(
				'target' => '_blank',
				'rel'    => 'nofollow',
				'class'  => '',
			), $params );

		// Add style class to break long urls:
		$params['class'] = trim( $params['class'].' linebreak' );

		$link = '<a href="'.$this->get_field_url().'"'.get_field_attribs_as_string( $params, false ).'>'.$this->get_field_url().'</a>';

		return $link;
	}


	/**
	 * Set param value
	 *
	 * @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 )
	{
		if( ( ! isset( $this->significant_changed_values[ $parname ] ) ) && ( ( $old_value = $this->get( $parname ) ) != $parvalue )
			&& in_array( $parname, array( 'login', 'grp_ID', 'nickname', 'firstname', 'lastname', 'gender', 'ctry_ID', 'rgn_ID', 'subrg_ID', 'city_ID' ) ) )
		{ // Save previous value of significant changes for later use in send_account_changed_notifications()
			$this->significant_changed_values[ $parname ] = $old_value;
		}

		switch( $parname )
		{
			case 'level':
				return $this->set_param( $parname, 'number', $parvalue, $make_null );

			case 'source':
				// Limit source with 30 char because of this DB field max length:
				return $this->set_param( $parname, 'string', utf8_substr( $parvalue, 0, 30 ), $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 'ctry_ID':
			default:
				return $this->set_param( $parname, 'string', $parvalue, $make_null );
		}
	}


	/**
	 * Set the encoded password and a new password salt from a raw password
	 *
	 * @param string the raw password
	 */
	function set_password( $raw_password )
	{
		// Use first password driver from config array for new password updating:
		$PasswordDriver = get_PasswordDriver();

		// Save temporarily current pass driver to know if it has been chaged:
		$this->previous_pass_driver = $this->get( 'pass_driver' );

		$this->set( 'pass', $PasswordDriver->hash( $raw_password ) );
		$this->set( 'salt', $PasswordDriver->get_last_generated_salt() );
		$this->set( 'pass_driver', $PasswordDriver->get_code() );
	}


	/**
	 * Set date created.
	 *
	 * @param integer seconds since Unix Epoch.
	 */
	function set_datecreated( $datecreated, $isYMDhour = false )
	{
		if( !$isYMDhour )
		{
			$datecreated = date('Y-m-d H:i:s', $datecreated );
		}
		// Set value:
		$this->datecreated = $datecreated;
		// Remmeber change for later db update:
		$this->dbchange( 'user_created_datetime', 'string', 'datecreated' );
	}


	/**
	 * Set email address of the user.
	 *
	 * If the email address has changed and we're configured to invalidate the user in this case,
	 * the user's account gets not active 'emaichanged' status here.
	 *
	 * @param string email address to set for the User
	 * @param boolean Set TRUE if changing of account status is required
	 * @return boolean true, if set; false if not changed
	 */
	function set_email( $email, $change_status = true )
	{
		global $Settings;

		$r = parent::set_param( 'email', 'string', utf8_strtolower( $email ) );

		if( $change_status )
		{ // Change user status to 'emailchanged' (if email has changed and Settings are available, which they are not during install):
			if( $r && ( $this->ID != 0 ) && isset($Settings) && $Settings->get('newusers_revalidate_emailchg') && ( $this->check_status( 'is_validated' ) ) )
			{ // Deactivate the account, because the changed email has not been verified yet, but the user account is active:
				$this->set( 'status', 'emailchanged' );
			}
		}

		if( preg_match( '#@(.+)#i', $email, $ematch ) )
		{	// Set email domain ID
			global $DB;

			$email_domain = $ematch[1];

			$SQL = new SQL( 'Get email domain' );
			$SQL->SELECT( 'dom_ID' );
			$SQL->FROM( 'T_basedomains' );
			$SQL->WHERE_and( 'dom_name = '.$DB->quote( $email_domain ) );

			$dom_ID = $DB->get_var( $SQL );
			if( !$dom_ID )
			{	// The email domains doesn't exist yet, Insert new record
				$DB->query( 'INSERT INTO T_basedomains ( dom_type, dom_name )
					VALUES ( \'email\', '.$DB->quote( $email_domain ).' )' );
				$dom_ID = $DB->insert_id;
			}

			$this->set( 'email_dom_ID', (int) $dom_ID );
		}

		return $r;
	}


	/**
	 * Set new Group.
	 *
	 * @param Group the Group object to put the user into
	 * @return boolean true if set, false if not changed
	 */
	function set_Group( & $Group )
	{
		if( $this->grp_ID !== $Group->ID )
		{
			$this->Group = & $Group;
			$this->set( 'grp_ID', $Group->ID );
			return true;
		}

		return false;
	}

	/**
	 * @deprecated by {@link User::set_Group()} since 1.9
	 */
	function setGroup( & $Group )
	{
		global $Debuglog;
		$Debuglog->add( 'Call to deprecated method User::setGroup(), use set_Group() instead.', 'deprecated' );
		return $this->set_Group( $Group );
	}


	/**
	 * Get the {@link Group} of the user.
	 *
	 * @return Group (by reference)
	 */
	function & get_Group()
	{
		if( ! isset($this->Group) )
		{
			$GroupCache = & get_GroupCache();
			$this->Group = & $GroupCache->get_by_ID($this->grp_ID);
		}
		return $this->Group;
	}


  /**
	 * Check password
	 *
	 * @param string Password
	 * @param boolean Is the password parameter already hashed?
	 * @return boolean
	 */
	function check_password( $pass, $pass_is_hashed = false )
	{
		// Get password driver for this user:
		$user_PasswordDriver = $this->get_PasswordDriver();

		if( ! $user_PasswordDriver )
		{	// Password driver cannot be detected for this user, so it cannot be checked:
			return false;
		}

		// Check the requested password against the user's password hash:
		return $user_PasswordDriver->check( $pass, $this->salt, $this->pass, $pass_is_hashed );
	}


	/**
	 * Check a specific permission for this User.
	 * This is the MAIN permission check function that you should call to check any permission.
	 * This function will delegate to other functions when appropriate.
	 *
	 * @param string Permission name, can be one of:
	 *                - 'edit_timestamp'
	 *                - 'cats_post_statuses', see {@link User::check_perm_catsusers()}
	 *                - either group permission names, see {@link Group::check_perm()}
	 *                - either blogusers permission names, see {@link User::check_perm_blogusers()}
	 * @param string Permission level
	 * @param boolean Execution will halt if this is !0 and permission is denied
	 * @param mixed Permission target (blog ID, array of cat IDs, Item, Comment...)
	 * @return boolean 0 if permission denied
	 */
	function check_perm( $permname, $permlevel = 'any', $assert = false, $perm_target = NULL )
	{
		global $Debuglog, $Settings;

		if( is_object($perm_target) && isset($perm_target->ID) )
		{
			$perm_target_ID = $perm_target->ID;
		}
		elseif( !is_array($perm_target) )
		{
			$perm_target_ID = $perm_target;
		}

		$cache_permname = $permname; // save original name because $permname can be changed below
		$cache_target_ID = isset( $perm_target_ID ) ? $perm_target_ID : 'null';
		if( isset( $this->cache_perms[$cache_permname][$permlevel][$cache_target_ID] ) )
		{	// Permission is available in Cache:
			$Debuglog->add( "Got perm [$cache_permname][$permlevel][$cache_target_ID] from cache", 'perms' );
			$perm = $this->cache_perms[$cache_permname][$permlevel][$cache_target_ID];
			if( ! $perm && $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 )." ($permname:$permlevel:".( is_object( $perm_target ) ? get_class( $perm_target ).'('.$perm_target_ID.')' : ( is_array( $perm_target ) ? implode( ', ', $perm_target ) : $perm_target ) ).")" );
			}
			return $perm;
		}


		$pluggable_perms = array( 'admin', 'spamblacklist', 'slugs', 'templates', 'options', 'files', 'users', 'orgs', 'polls' );
		if( in_array( $permname, $pluggable_perms ) )
		{
			$permname = 'perm_'.$permname;
		}
		//$Debuglog->add( "Querying perm [$permname][$permlevel]".( isset( $perm_target_ID ) ? '['.$perm_target_ID.']' : '' ).']', 'perms' );
		//pre_dump( 'Perm target: '.var_export( $perm_target, true ) );

		$perm = false;

		switch( $permname )
		{ // What permission do we want to check?
			case 'cats_post_statuses':
			case 'cats_post!published':
			case 'cats_post!community':
			case 'cats_post!protected':
			case 'cats_post!private':
			case 'cats_post!review':
			case 'cats_post!draft':
			case 'cats_post!deprecated':
			case 'cats_post!redirected':
			case 'cats_item_type_standard':
			case 'cats_item_type_restricted':
			case 'cats_item_type_admin':
				// Category permissions...
				if( ! is_array( $perm_target ) )
				{	// We need an array here:
					$perm_target = array( $perm_target );
				}

				// First we need to create an array of blogs, not cats
				$perm_target_blogs = array();
				foreach( $perm_target as $loop_cat_ID )
				{
					$loop_cat_blog_ID = get_catblog( $loop_cat_ID );
					// echo "cat $loop_cat_ID -> blog $loop_cat_blog_ID <br />";
					if( ! in_array( $loop_cat_blog_ID, $perm_target_blogs ) )
					{ // not already in list: add it:
						$perm_target_blogs[] = $loop_cat_blog_ID;
					}
				}

				$perm = true; // Permission granted if no blog denies it below
				$blogperm = 'blog_'.substr( $permname, 5 );
				// Now we'll check permissions for each blog:
				foreach( $perm_target_blogs as $loop_blog_ID )
				{
					if( ! $this->check_perm( $blogperm, $permlevel, false, $loop_blog_ID ) )
					{ // If at least one blog denies the permission:
						$perm = false;
						break;
					}
				}
				break;

			case 'recycle_owncmts':
				// Check permission to edit comments for own items
				$Comment = & $perm_target;
				$Item = & $Comment->get_Item();
				$blog_ID = $Item->get_blog_ID();
				if( $Item->creator_user_ID == $this->ID )
				{ // Current user is owner of this item
					if( $Item->is_locked() && !$this->check_perm( 'blog_cats', 'edit', false, $blog_ID ) )
					{ // Comment item is locked and current user is not allowed to edit locked items comment
						break;
					}

					$comment_author_User = & $Comment->get_author_User();
					if( ( empty( $comment_author_User ) || ( $comment_author_User->level <= $this->level ) )
						&& in_array( $Comment->status, array( 'published', 'community', 'protected' ) ) )
					{ // Comment author is anonymous or his level is lower than current User level, and the Comment was published with some of the above statuses
						// Check blog user perms to see if user may recycle his own posts comments
						$perm = $this->check_perm_blogusers( 'blog_recycle_owncmts', $permlevel, $blog_ID );
						if( ! $perm )
						{	// Check primary and secondary groups for permissions to this specific collection:
							$perm = $this->check_perm_bloggroups( 'blog_recycle_owncmts', $permlevel, $blog_ID );
						}
					}
				}
				break;

			// Permissions on a collection:
			// NOTE: these are currently the only collections that will check multiple user groups:
			case 'blog_ismember':
			case 'blog_workflow_status':
			case 'blog_workflow_user':
			case 'blog_workflow_priority':
			case 'blog_item_propose':
			case 'blog_post_statuses':
			case 'blog_post!published':
			case 'blog_post!community':
			case 'blog_post!protected':
			case 'blog_post!private':
			case 'blog_post!review':
			case 'blog_post!draft':
			case 'blog_post!deprecated':
			case 'blog_post!redirected':
			case 'blog_del_post':
			case 'blog_edit':
			case 'blog_edit_cmt':
			case 'blog_comments':
			case 'blog_comment_statuses':
			case 'blog_del_cmts':
			case 'blog_vote_spam_comments':
			case 'blog_comment!published':
			case 'blog_comment!community':
			case 'blog_comment!protected':
			case 'blog_comment!private':
			case 'blog_comment!deprecated':
			case 'blog_comment!review':
			case 'blog_comment!draft':
			case 'blog_properties':
			case 'blog_cats':
			case 'blog_item_type_standard':
			case 'blog_item_type_restricted':
			case 'blog_item_type_admin':
			case 'blog_edit_ts':
			case 'blog_media_browse':
			case 'blog_analytics':
				// The owner of a collection has automatic permission to so many things:
				if( $this->check_perm_blogowner( $perm_target_ID ) )
				{	// Owner can do *almost* anything:
					$perm = true;
					break;
				}
				/* continue */
			case 'blog_admin': // This is what the collection owner does not have automatic access to!

				// Group may grant VIEW access, FULL access:
				$group_permlevel = ( $permlevel == 'view' ||  $permlevel == 'any' ) ? $permlevel : 'editall';

				$primary_Group = & $this->get_Group();
				if( $primary_Group->check_perm( 'blogs', $group_permlevel ) )
				{	// Primary usergroup grants a global permission:
					$perm = true;
					// Stop checking other perms:
					break;
				}

			case 'blog_can_be_assignee':
				// Apply next rules below for above collection permissions if current User is not admin or not owner of the Collection:
				// NOTE: The permission "Workflow Member (Items can be assigned to this User/Group)" may be disabled even for admin!

				if( $perm_target_ID > 0 )
				{	// Check the permissions below only for requested target collection:

					if( $this->check_perm_bloggroups( $permname, $permlevel, $perm_target_ID ) )
					{	// Primary or Secondary advanced usergroups permissions on the target collection grant the requested permission:
						$perm = true;
						// Stop checking other perms:
						break;
					}

					// Check user specific perms:
					if( $this->check_perm_blogusers( $permname, $permlevel, $perm_target_ID ) )
					{	// Advanced user permissions on the target collection grant the requested permission:
						$perm = true;
						// Stop checking other perms:
						break;
					}

					if( $permname == 'blog_properties' && $permlevel == 'copy' )
					{	// Check if user can copy the selected collection:
						$BlogCache = & get_BlogCache();
						if( $perm_Blog = & $BlogCache->get_by_ID( $perm_target_ID, false, false ) )
						{
							if( $this->check_perm( 'blogs', 'create', $assert, $perm_Blog->get( 'sec_ID' ) ) )
							{	// If user can create new collection in section of the duplicating collection:
								if( $perm_Blog->get_setting( 'allow_duplicate' ) ||
								    $this->check_perm( 'blog_properties', 'edit', false, $perm_Blog->ID ) )
								{	// If the collection is allowed to duplicate by anyone OR user can edit the duplicating collection:
									$perm = true;
								}
							}
						}
					}
				}

				break;

			case 'comment!CURSTATUS':
				/**
				 * @var Comment
				 */
				$Comment = & $perm_target;
				// Change the permname to one of the following:
				$permname = 'comment!'.$Comment->status;
			case 'comment!published':
			case 'comment!community':
			case 'comment!protected':
			case 'comment!private':
			case 'comment!review':
			case 'comment!draft':
			case 'comment!deprecated':
			case 'comment!trash':
				/**
				 * @var Comment
				 */
				$Comment = & $perm_target;
				$Item = & $Comment->get_Item();
				$blog_ID = $Item->get_blog_ID();
				$check_status = substr( $permname, 8 );

				if( $Comment->is_meta() && in_array( $permlevel, array( 'edit', 'moderate', 'delete' ) ) )
				{ // Check the permissions for internal comment with special function
					$perm = $this->check_perm( 'meta_comment', $permlevel, false, $Comment );
					break;
				}

				if( ( ( $permlevel != 'view' ) &&  $Item->is_locked() && !$this->check_perm( 'blog_cats', 'edit', false, $blog_ID ) )
						&& ! ( $permlevel == 'delete' && $this->check_perm( 'users', 'edit' ) ) )
				{ // Comment item is locked, i.e., all of its categories are locked, and current user is not allowed to edit/moderate locked items comment or is not a user admin
					break;
				}

				if( $this->check_perm_blog_global( $blog_ID, $permlevel ) )
				{ // User has global permission on this blog:
					$perm = true;
					break;
				}

				if( $Comment->status == 'trash' )
				{ // only global group 'editall' perm can give rights to 'trash' status, but this is not the case
					break;
				}

				if( $permlevel == 'delete' )
				{ // permlevel is delete so we have to check the 'blog_del_cmts' permission
					$perm = $this->check_perm( 'blog_del_cmts', 'edit', false, $blog_ID )
							|| $this->check_perm( 'recycle_owncmts', $permlevel, false, $Comment )
							|| $this->check_perm( 'users', 'edit', false );
					break;
				}

				// Check comment current status permissions at the blog level:
				$blog_permname = 'blog_comment!'.$Comment->status;
				$perm = $perm || $this->check_perm_blogusers( $blog_permname, $permlevel, $blog_ID, $Comment );
				if( ! $perm )
				{	// Check primary and secondary groups for permissions to this specific collection:
					$perm = $this->check_perm_bloggroups( $blog_permname, $permlevel, $blog_ID, $Comment, $this );
				}
				if( $perm && ( $Comment->status != $check_status ) )
				{ // also check the requested status permissions at the blog level
					$blog_permname = 'blog_comment!'.$check_status;
					$perm = $this->check_perm_blogusers( $blog_permname, 'create', $blog_ID );
					if( ! $perm )
					{	// Check primary and secondary groups for permissions to this specific collection:
						$perm = $this->check_perm_bloggroups( $blog_permname, 'create', $blog_ID );
					}
				}
				break;

			case 'meta_comment':
				// Check permission for internal comment:

				if( $permlevel == 'edit' || $permlevel == 'delete' )
				{	// Set Comment from target object:
					$Comment = & $perm_target;
					if( empty( $Comment ) || ! $Comment->is_meta() )
					{	// Comment must be defined and meta to check these permissions:
						$perm = false;
						break;
					}
					$Item = & $Comment->get_Item();
					$blog_ID = $Item->get_blog_ID();
				}
				elseif( $permlevel == 'view' || $permlevel == 'add' || $permlevel == 'blog' )
				{	// Set blog ID from target value:
					$blog_ID = $perm_target_ID;
				}
				else
				{	// Permission level is not allowed for all internal comments (For example 'moderate')
					$perm = false;
					break;
				}

				if( empty( $blog_ID ) )
				{	// Item or Blog must be defined to check these permissions:
					$perm = false;
					break;
				}

				if( $this->check_perm_blog_global( $blog_ID, $permlevel ) )
				{	// User has global permission on this collection:
					$perm = true;
					break;
				}

				switch( $permlevel )
				{
					case 'view':
					case 'blog': // deprecated perm level
						// Check perms to View internal comments of the collection:
						$perm = // If User is explicitly allowed in the user permissions
								$this->check_perm_blogusers( 'meta_comment', 'view', $blog_ID )
								// OR If User belongs to primary or secondary groups explicitly allowed in the group permissions
								|| $this->check_perm_bloggroups( 'meta_comment', 'view', $blog_ID );
						break;

					case 'add':
						// Check perms to Add internal comments to the collection items:
						$perm = // If User can view internal comment of the collection
								$this->check_perm( 'meta_comment', 'view', false, $blog_ID );
						break;

					case 'edit':
						// Check perms to Edit internal comment:
						$perm = // If User is explicitly allowed in the user permissions
								$this->check_perm_blogusers( 'meta_comment', 'edit', $blog_ID, $Comment )
								// OR If User belongs to primary or secondary groups explicitly allowed in the group permissions
								|| $this->check_perm_bloggroups( 'meta_comment', 'edit', $blog_ID, $Comment, $this );
						break;

					case 'delete':
						// Check perms to Delete internal comments:
						$perm = // If it is allowed to delete comments on the collection
								$this->check_perm( 'blog_del_cmts', 'edit', false, $blog_ID )
								// AND if user can view internal comment of the collection
								&& $this->check_perm( 'meta_comment', 'view', false, $blog_ID );
						break;
				}

				break;

			case 'item_post!CURSTATUS':
				/**
				 * @var Item
				 */
				$Item = & $perm_target;
				// Change the permname to one of the following:
				$permname = 'item_post!'.$Item->status;
			case 'item_post!published':
			case 'item_post!community':
			case 'item_post!protected':
			case 'item_post!private':
			case 'item_post!review':
			case 'item_post!draft':
			case 'item_post!deprecated':
			case 'item_post!redirected':
				// Get the Blog ID
				/**
				 * @var Item
				 */
				$Item = & $perm_target;
				$blog_ID = $Item->get_blog_ID();
				$check_status = substr( $permname, 10 );

				if( ( ( $permlevel != 'view' ) && $Item->is_locked() && !$this->check_perm( 'blog_cats', 'edit', false, $blog_ID ) )
						&& ! ( $permlevel == 'delete' && $this->check_perm( 'users', 'edit', false ) ) )
				{ // Item is locked, i.e., it has all of its categories locked, and current user is not allowed to edit locked items
					// ( only view permission is allowed by default for locked items ) or is not a user admin
					break;
				}

				if( $this->check_perm_blog_global( $blog_ID, $permlevel ) )
				{ // User has global permission on this blog:
					$perm = true;
					break;
				}

				if( $permlevel == 'delete' )
				{ // permlevel is delete so we have to check the 'blog_del_post' permission
					// User admins are allowed to delete posts
					$perm = $this->check_perm( 'blog_del_post', 'edit', false, $blog_ID ) || $this->check_perm( 'users', 'edit', false );
					break;
				}

				// Check permissions at the blog level:
				$blog_permname = 'blog_post!'.$Item->status;
				$perm = $this->check_perm_blogusers( $blog_permname, $permlevel, $blog_ID, $Item );
				if( ! $perm )
				{	// Check primary and secondary groups for permissions to this specific collection:
					$perm = $this->check_perm_bloggroups( $blog_permname, $permlevel, $blog_ID, $Item, $this );
				}
				if( $perm && ( $Item->status != $check_status ) )
				{ // also check the requested status permissions at the blog level
					$blog_permname = 'blog_post!'.$check_status;
					$perm = $this->check_perm_blogusers( $blog_permname, 'create', $blog_ID );
					if( ! $perm )
					{	// Check primary and secondary groups for permissions to this specific collection:
						$perm = $this->check_perm_bloggroups( $blog_permname, 'create', $blog_ID );
					}
				}
				break;

			case 'stats':
				// Blog permission to edit its properties...
				$this->get_Group();

				// Group may grant VIEW access, FULL access:
				if( $this->Group->check_perm( $permname, $permlevel, $perm_target ) )
				{ // If group grants a global permission:
					$perm = true;
					break;
				}

				if( $perm_target > 0 )
				{ // Check user perm for this blog:
					$perm = $this->check_perm_blogusers( $permname, $permlevel, $perm_target );
					if ( ! $perm )
					{	// Check primary and secondary groups for permissions to this specific collection:
						$perm = $this->check_perm_bloggroups( $permname, $permlevel, $perm_target );
					}
				}
				elseif( $permlevel == 'list' && $perm_target == NULL )
				{	// Check if at least one collection has a perm to view analytics for this user:
					$perm = check_coll_first_perm( 'perm_analytics', 'user', $this->ID );
				}
				break;

			case 'user':
				// Check if user can 'view'/'moderate'/'edit'/'delete' another user
				$User = & $perm_target;
				if( !$this->check_status( 'can_view_user', $User->ID ) )
				{ // User can't even view the edited User
					break;
				}

				// If user has global permission on all users then everything is allowed
				if( $this->check_perm( 'users', 'edit' ) )
				{ // Check if user can edit/delete other users
					$perm = true;
					break;
				}

				if( $permlevel == 'delete' || $permlevel == 'edit' )
				{ // These permission levels are not allowed for this user
					break;
				}

				if( ( ! $Settings->get('allow_anonymous_user_profiles') ) && ( ! $this->check_perm( 'cross_country_allow_profiles' ) ) && ( empty( $this->ctry_ID ) || ( $this->ctry_ID !== $User->ctry_ID ) ) )
				{ // Users can view/browse other users only from the same country, but this user country is empty or not the same as the target user country
					// this User has no permission to even view the target user
					$User->get_Group();
					if( $User->Group->level < $Settings->get('allow_anonymous_user_level_min') ||
					    $User->Group->level > $Settings->get('allow_anonymous_user_level_max') )
					{ // The anonymous users have no access to view this user
						break;
					}
				}

				// Note: With this implementation only admin users are allowed to view users from different countries when cross country browsing is restricted
				// check if user has permission to moderate another user
				if( ( $permlevel == 'moderate' ) && $this->check_perm( 'users', 'moderate' ) )
				{ // this user has moderator permission, check if the group level is higher then the target user group level
					$this->get_Group();
					$User->get_Group();
					$perm = ( $this->Group->level > $User->Group->level );
					break;
				}

				// Only 'view' permlevel remained, and this user is allowed to view the target user
				$perm = true;
				break;

			// asimo> edit_timestamp permission was converted to blog_edit_ts permission


			// asimo> files permission was converted to pluggable permission
			/*case 'files':
				$this->get_Group();
				$perm = $this->Group->check_perm( $permname, $permlevel );*/

				/* Notes:
				 *  - $perm_target can be:
				 *    - NULL or 0: check global group permission only
				 *    - positive: check global group permission and
				 *      (if granted) if a specific blog denies it.
* fp> This is BAD BAD BAD because it's inconsistent with the other permissions
* in b2evolution. There should NEVER be a denying. ony additional allowing.
* It's also inconsistent with most other permission systems.
* The lower file permission level for groups is now called "No Access"
* This should be renamed to "Depending on each blog's permissions"
* Whatever general permissions you have on files, blog can give you additional permissions
* but they can never take a global perm away.
* Tblue> On the permissions page it says that the blog perms will be restricted
* by any global perms, which means to me that a blog cannot grant e. g.
* the files upload perm if this perm isn't granted globally... But apparently
* it shouldn't be like that?! I understand it should be like that then:
* if( ! $perm && $perm_target && in_array( $permlevel, array( 'add', 'view', 'edit' ) )
* {
* 		// check if blog grants permission.
* }
* If this is correct, we should remove the note on the blog permissions
* pages and the group properties form.
* fp> ok, I had forgotten we had that old message, but still it doesn't say it will, it says it *may* !
* To be exact the message should be "
* Note: General group permissions may further restrict or extend any permissions defined here."
* Restriction should only happen when "NO ACCESS" is selected
* But when "Depending on each blog's permissions" is selected, THEN (and I guess ONLY then) the blog permissions should be used
* Note: This is quite messy actually. maybe it would make more sense to separate group permissions by "root type":
* i-e nto use the same permission for blog roots vs user root vs shared root vs skins root
* what do you think?
* Tblue> That sounds OK. So we would add another option to the global
* 'files' group perm setting ("Depending on each blog's permissions"), right?
* fp> yes.
* tb> Regarding separation: It could make sense. The blog-specific permissions would only
* affect blog roots (and if "Depending on each blog's permissions" is selected;
* for the other roots we would add separate (global) settings...
* fp> yes.
				 *  - Only a $permlevel of 'add', 'view' or 'edit' can be
				 *    denied by blog permissions.
				 *  - If the group grants the 'all' permission, blogs cannot
				 *    deny it.
				 */
/*
				if( $perm && $perm_target && in_array( $permlevel, array( 'add', 'view', 'edit' ) )
					&& $this->Group->get( 'perm_files' ) != 'all' )
				{	// Check specific blog perms:
					$perm = $this->check_perm_blogusers( $permname, $permlevel, $perm_target );
					if ( ! $perm )
					{	// Check primary and secondary groups for permissions for this specific collection:
						$perm = $this->check_perm_bloggroups( $permname, $permlevel, $perm_target );
					}
				}
*/
				//break;

			default:
				// Check pluggable permissions using user permission check function
				if( $permname == 'perm_users' && ! $this->check_perm( 'admin', 'normal' ) )
				{	// User must has Normal Back-office Access to view/moderate/edit other users:
					$perm = false;
					break;
				}
				$this->get_Group();
				$perm = Module::check_perm( $permname, $permlevel, $perm_target, 'user_func', $this->Group );
				if( $perm === true || $perm === NULL )
				{ // If user_func or user perm not exists in the corresponding module then $perm value will be NULL and we have to check the group permission.
					// If user_func exists and returns true, then we have to check group permission to make sure it does not restrict the user perm.

					// Other global permissions (see if the group can handle them).
					// Forward request to group:
					$perm = $this->Group->check_perm( $permname, $permlevel, $perm_target, $this );
				}
		}

		if( is_object($perm_target) )
		{	// Prevent catchable E_FATAL with PHP 5.2 (because there's no __tostring for e.g. Item)
			$taget_name = get_class($perm_target).'('.$perm_target_ID.')';
		}
		elseif( is_array($perm_target) )
		{	// Convert to ID list
			$taget_name = '('.implode(',', $perm_target).')';
		}
		else
		{
			$taget_name = $perm_target;
		}

		$Debuglog->add( 'User perm '.$permname.':'.$permlevel.':'.$taget_name.' => '.($perm ? 'granted' : 'DENIED'), 'perms' );

		if( ! $perm && $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 )." ($permname:$permlevel:".( is_object( $perm_target ) ? get_class( $perm_target ).'('.$perm_target_ID.')' : ( is_array( $perm_target ) ? implode( ', ', $perm_target ) : $perm_target ) ).")" );
		}

		// Cache result:
		$this->cache_perms[$cache_permname][$permlevel][$cache_target_ID] = $perm;

		return $perm;
	}


	/**
	 * Check if the user is the owner of the designated blog (which gives him a lot of permissions)
	 *
	 * @param integer
	 * @return boolean
	 */
	function check_perm_blogowner( $blog_ID )
	{
		if( empty( $blog_ID ) )
		{
			return false;
		}

		$BlogCache = & get_BlogCache();
		/**
		 * @var Blog
		 */
		$Collection = $Blog = & $BlogCache->get_by_ID( $blog_ID );

		return ( $Blog->owner_user_ID == $this->ID );
	}


	/**
	 * Check if the user is the owner of the designated item (which gives him a lot of permissions)
	 *
	 * @param integer
	 * @return boolean
	 */
	function check_perm_itemowner( $item_ID )
	{
		if( empty( $item_ID ) )
		{
			return false;
		}

		$ItemCache = & get_ItemCache();
		/**
		 * @var Item
		 */
		$Item = & $ItemCache->get_by_ID( $item_ID );

		return ( $Item->creator_user_ID == $this->ID );
	}


	/**
	 * Check if user has global perms on this blog
	 *
	 * @param integer blog ID
	 * @param string permlevel
	 * @return boolean true if user is the owner, or user group has some permission on all blogs
	 */
	function check_perm_blog_global( $blog_ID, $permlevel = 'edit' )
	{
		if( $this->check_perm_blogowner( $blog_ID ) )
		{ // user is the blog owner
			return true;
		}

		// Group may grant VIEW access, FULL access:
		$this->get_Group();
		$group_permlevel = ( $permlevel == 'view' || $permlevel == 'any' ) ? 'viewall' : 'editall';
		if( $this->Group->check_perm( 'blogs', $group_permlevel ) )
		{ // If group grants a global permission:
			return true;
		}

		return false;
	}


	/**
	 * Check permission for this user on a specified blog
	 *
	 * This is not for direct use, please call {@link User::check_perm()} instead
	 *
	 * @see User::check_perm()
	 * @param string Permission name, can be one of the following:
	 *                  - blog_ismember
	 *                  - blog_can_be_assignee
	 *                  - blog_workflow_status
	 *                  - blog_workflow_user
	 *                  - blog_workflow_priority
	 *                  - blog_post_statuses
	 *                  - blog_del_post
	 *                  - blog_edit_ts
	 *                  - blog_comments
	 *                  - blog_cats
	 *                  - blog_properties
	 * @param string Permission level
	 * @param integer Permission target blog ID
	 * @param Item Item that we want to edit
	 * @return boolean 0 if permission denied
	 */
	function check_perm_blogusers( $permname, $permlevel, $perm_target_blog, $perm_target = NULL )
	{
		// Check if user blog advanced perms are loaded
		if( ! isset( $this->blog_post_statuses[$perm_target_blog] ) )
		{ // Blog post statuses have not been loaded yet:
			$this->blog_post_statuses[$perm_target_blog] = array();
			if( ! load_blog_advanced_perms( $this->blog_post_statuses[$perm_target_blog], $perm_target_blog, $this->ID, 'bloguser' ) )
			{ // Could not load blog advanced user perms
				return false;
			}
		}

		// Check permission and return the result
		return check_blog_advanced_perm( $this->blog_post_statuses[$perm_target_blog], $this->ID, $permname, $permlevel, $perm_target );
	}


	/**
	 * Check permission for primary and secondary groups of this user on a specified collection
	 *
	 * This is not for direct use, please call {@link User::check_perm()} instead
	 *
	 * @see User::check_perm()
	 * @param string Permission name
	 * @param string Permission level
	 * @param integer Permission target blog ID
	 * @param object Item that we want to edit
	 * @param object User for who we would like to check this permission
	 * @return boolean false if permission denied
	 */
	function check_perm_bloggroups( $permname, $permlevel, $perm_target_blog, $perm_target = NULL, $User = NULL )
	{
		// Get primary group of this User:
		$primary_Group = & $this->get_Group();

		if( $primary_Group->check_perm_bloggroups( $permname, $permlevel, $perm_target_blog, $perm_target, $User ) )
		{	// Primary advanced usergroup permissions on the target collection grant the requested permission:
			return true;
			// Stop checking other perms.
		}

		// Get secondary usergroups for this User:
		$secondary_groups = $this->get_secondary_groups();

		if( empty( $secondary_groups ) )
		{	// Stop checking other perms if this User has no secondary groups:
			return false;
		}

		// Check which secondary usergroup's advanced permissions must still be loaded: (we may have some in cache already)
		$notloaded_secondary_group_IDs = array();
		foreach( $secondary_groups as $secondary_Group )
		{
			if( ! isset( $secondary_Group->blog_post_statuses[ $perm_target_blog ] ) )
			{	// We must still load advanced permissions for this secondary usergroup:
				$notloaded_secondary_group_IDs[] = $secondary_Group->ID;
			}
		}

		if( count( $notloaded_secondary_group_IDs ) )
		{	// Load advanced permissions of secondary usergroups in a single query:
			$coll_advanced_perms = NULL;
			load_blog_advanced_perms( $coll_advanced_perms, $perm_target_blog, $notloaded_secondary_group_IDs, 'bloggroup' );
		}

		// Find first secondary usergroup that grants the required permission:
		foreach( $secondary_groups as $secondary_Group )
		{
			if( isset( $coll_advanced_perms, $coll_advanced_perms[ $secondary_Group->ID ] ) )
			{	// Set advanced usergroup permissions from array loaded above:
				$secondary_Group->blog_post_statuses[ $perm_target_blog ] = $coll_advanced_perms[ $secondary_Group->ID ];
			}
			if( $secondary_Group->check_perm_bloggroups( $permname, $permlevel, $perm_target_blog, $perm_target, $User ) )
			{	// Secondary usergroup grants requested permissions on the target collection:
				return true;
				// Stop checking other groups and other perms.
			}
		}

		// All groups of this User have no requested permission on a specified collection:
		return false;
	}


	/**
	 * Check if user status permits the requested action
	 *
	 * @param string requested action
	 * @param integger target ID - can be a post ID, user ID
	 * @return boolean true if the action is permitted, false otherwise
	 */
	function check_status( $action, $target = NULL )
	{
		global $Settings, $Collection, $Blog;

		switch( $action )
		{
			case 'can_view_user':
				if( $Settings->get( 'allow_anonymous_user_profiles' ) || ( !empty( $target ) && ( $target == $this->ID ) ) )
				{ // even anonymous users can see users profile, or user wants to check his own profile
					return true;
				}
				return ( ( $this->status == 'activated' ) || ( $this->status == 'autoactivated' ) || ( $this->status == 'manualactivated' ) );
			case 'can_view_users':
				if( $Settings->get( 'allow_anonymous_user_list' ) )
				{ // even anonymous users can see users list
					return true;
				}
				return ( ( $this->status == 'activated' ) || ( $this->status == 'autoactivated' ) || ( $this->status == 'manualactivated' ) );
			case 'can_view_comments':
			case 'can_view_contacts':
			case 'can_view_messages':
			case 'can_view_threads':
			case 'is_validated':
			case 'can_access_admin':
			case 'can_edit_post':
			case 'can_edit_comment':
			case 'can_edit_contacts':
				return ( ( $this->status == 'activated' ) || ( $this->status == 'autoactivated' ) || ( $this->status == 'manualactivated' ) );
			case 'can_report_user':
				if( ! empty( $target ) )
				{
					$UserCache = & get_UserCache();
					$target_User = & $UserCache->get_by_ID( $target );
					return ( ( $this->status == 'activated' ) || ( $this->status == 'autoactivated' ) || ( $this->status == 'manualactivated' ) )
							&& ( ! empty( $target_User ) && ( $target_User->get( 'level' ) <= $this->get( 'level' ) ) );
				}
				return false;
			case 'can_be_validated':
				return in_array( $this->status, array( 'new', 'emailchanged', 'deactivated', 'failedactivation', 'pendingdelete' ) );
			case 'can_view_msgform':
			case 'can_receive_any_message': // can this user receive emails or private messages
			case 'can_receive_pm':
			case 'can_display_avatar': // can display user's avatar for not admin users
			case 'can_display_link': // can display user's profile link for not admin users
				return ( $this->status != 'closed' );
			case 'is_closed':
				return ( $this->status == 'closed' );
			default:
				debug_die( 'This action is not handled during status check!' );
		}
	}

	/**
	 * Check if the user has the given role in any blog
	 *
	 * @param string role name, available values ( post_owner, moderator )
	 * @return mixed NULL if the given roll name is not defined or there are no blogs, true if the user is super admin, 0 if the user doesn't have the given role, positive number otherwise
	 */
	function check_role( $rolename )
	{
		global $DB;

		if( $this->check_perm( 'blogs', 'editall' ) )
		{ // if user has global editall blogs permission then it has any kind of role in all blogs
			return true;
		}

		switch( $rolename )
		{
			case 'post_owner':
				// User is considerated as a post owner, if already has at least one post, or he has right to create posts
				if( $this->get_num_posts() > 0 )
				{ // User already has at least one post
					return true;
				}
				$role_conditions = array( 'perm_poststatuses' => array( 'IS NOT NULL', '<> ""' ) );
				break;

			case 'member':
				// User has member role if is member of at least one blog
				$role_conditions = array( 'ismember' => array( 'IS NOT NULL', '<> 0' ) );
				break;

			case 'comment_editor':
				// User has permission to edit some other users comments at least in one status
				$role_conditions = array(
					'perm_edit_cmt' => array( 'IS NOT NULL', '<> "no"', '<> "own"' ),
					'perm_cmtstatuses' => array( 'IS NOT NULL', '<> 0' )
				);
				break;

			case 'comment_moderator':
				// set comment moderator perm names
				$edit_perm_name = 'perm_edit_cmt';
				$statuses_perm_name = 'perm_cmtstatuses';
			case 'post_moderator':
				if( $rolename == 'post_moderator' )
				{ // set post moderator perm names
					$edit_perm_name = 'perm_edit';
					$statuses_perm_name = 'perm_poststatuses';
				}
				// User is a moderator if has moderator permission at least in one blog
				// A moderator must have permissions to create post/comment with at least two statuses from moderation statuses + published status
				$check_statuses = get_visibility_statuses( 'moderation' );
				// Create addition of statuses perm values
				$perms_value = get_status_permvalue( 'published' );
				foreach( $check_statuses as $status )
				{
					$perms_value = $perms_value + get_status_permvalue( $status );
				}

				// Check if user has permission to edit other comments than his own and has create permission on at least two statuses defined above
				$role_conditions = array(
					$edit_perm_name => array( 'IS NOT NULL', '<> "no"', '<> "own"' ),
					$statuses_perm_name => array( 'IS NOT NULL', 'BIT_COUNT( $perm_field$ & '.$perms_value.' ) > 1' )
				);
				break;

			default: // roll with the given roll name is not defined
				return NULL;
		}

		$where_clause = '';
		$perm_prefixes = array( 'bloguser_', 'bloggroup_' );
		foreach( $perm_prefixes as $prefix )
		{ // Check requred perms on blogusers and bloggroups as well
			$where_part = '';
			foreach( $role_conditions as $perm_name => $conditions )
			{ // Go through each required permission
				$perm_field = $prefix.$perm_name;
				foreach( $conditions as $condition )
				{ // Check all defined conditions and join with 'AND' operator
					if( strpos( $condition, '$perm_field$' ) !== false )
					{ // The $perm_filed must be replaced in the middle of the condition
						$where_part .= '( '.str_replace( '$perm_field$', $perm_field, $condition ).' ) AND ';
					}
					else
					{ // The $perm_filed must be added into the beginning of the condition
						$where_part .= '( '.$perm_field.' '.$condition.' ) AND ';
					}
				}
			}
			// Remove the last ' AND ' from the end of this where clause part
			$where_part = substr( $where_part, 0, strlen( $where_part ) - 5 );
			// Add the created conditions to the final where clause
			$where_clause .= '( '.$where_part.' )';
			if( $prefix != 'bloggroup_' )
			{ // 'bloggroup_' perm check is the last, but everywhere else we need an 'OR' operator
				$where_clause .= ' OR ';
			}
		}

		// Count blog ids where this user has the required permissions for the given role
		$SQL = new SQL( 'Check user role in all collections' );
		$SQL->SELECT( 'COUNT( DISTINCT blog_ID )' );
		$SQL->FROM( 'T_blogs' );
		$SQL->FROM_add( 'LEFT JOIN T_coll_user_perms ON (blog_advanced_perms <> 0 AND blog_ID = bloguser_blog_ID AND bloguser_user_ID = '.$this->ID.' )' );
		$SQL->FROM_add( 'LEFT JOIN T_coll_group_perms ON ( blog_advanced_perms <> 0
			AND blog_ID = bloggroup_blog_ID
			AND ( bloggroup_group_ID = '.$this->grp_ID.'
			      OR bloggroup_group_ID IN ( SELECT sug_grp_ID FROM T_users__secondary_user_groups WHERE sug_user_ID = '.$this->ID.' ) )
			)' );
		$SQL->WHERE( 'blog_owner_user_ID = '.$this->ID );
		$SQL->WHERE_or( $where_clause );

		return $DB->get_var( $SQL );
	}


	/**
	 * Check if this user and his group accept receiving private messages or not
	 *
	 * @return boolean
	 */
	function accepts_pm()
	{
		global $UserSettings;
		return ( $UserSettings->get( 'enable_PM', $this->ID ) && $this->check_perm( 'perm_messaging', 'reply' ) );
	}


	/**
	 * Check if this user accepts receiving emails and has an email address
	 *
	 * @return boolean
	 */
	function accepts_email()
	{
		global $UserSettings, $Settings;
		return ( $Settings->get( 'emails_msgform') != 'never' )
			&& ( $UserSettings->get( 'enable_email', $this->ID ) )
			&& ( ! empty( $this->email ) );
	}


	/**
	 * Check if user is owner of at least one collection
	 *
	 * @return boolean
	 */
	function is_collection_owner()
	{
		if( is_null( $this->is_collection_owner ) )
		{	// Get a result from DB first time and put in cache var:
			global $DB;

			$check_owner_SQL = new SQL( 'Check if user #'.$this->ID.' is owner of at least one collection' );
			$check_owner_SQL->SELECT( 'blog_ID' );
			$check_owner_SQL->FROM( 'T_blogs' );
			$check_owner_SQL->WHERE( 'blog_owner_user_ID = '.$DB->quote( $this->ID ) );
			$check_owner_SQL->LIMIT( '1' );

			$this->is_collection_owner = ($DB->get_var( $check_owner_SQL ) ? true : false);
		}

		return $this->is_collection_owner;
	}


	/**
	 * Get messaging possibilities between current user and $this user
	 *
	 * @param object Current User (the one trying to send the PM)
	 * @param string Type of contact method to check first: 'PM' > 'email'
	 * @return NULL|string allowed messaging possibility: PM > email > login > NULL
	 */
	function get_msgform_possibility( $current_User = NULL, $check_level = 'PM' )
	{
		if( ! $this->is_collection_owner() && ! $this->check_status( 'can_receive_any_message' ) )
		{ // In case of a closed account:
			return NULL;
		}

		if( is_logged_in() )
		{ // current User is a registered user/logged in:

			if( $current_User == NULL )
			{
				global $current_User;
			}

			// Check cross-country restrictions:
			if( ! $this->is_collection_owner() && has_cross_country_restriction( 'contact' ) && ( empty( $current_User->ctry_ID ) || ( $current_User->ctry_ID !== $this->ctry_ID ) ) )
			{ // Contact to this user is not enabled
				return NULL;
			}

			if( $check_level == 'PM' && $this->accepts_pm() && $current_User->accepts_pm() && ( $this->ID != $current_User->ID ) )
			{	// Both users have permission to send or receive Private Messages and not the same user...
				// Check if current_User is allowed to create a new conversation with this user:
				$blocked_contact = check_blocked_contacts( array( $this->ID ) );
				if( empty( $blocked_contact ) && ! $current_User->must_accept_terms() )
				{ // User is allowed to send PM to this user, and he didn't reach his new thread limit yet:
					return 'PM';
				}
			}

			if( $this->is_collection_owner() || $this->accepts_email() )
			{ // This User allows email => send email OR Force to allow to contact with this user because he is owner of the selected collection:
				return 'email';
			}

		}
		else
		{ // current User is anonymous/not logged in:

			if( $this->is_collection_owner() || $this->accepts_email() )
			{ // This User allows email => send email OR Force to allow to contact with this user because he is owner of the selected collection:
				return 'email';
			}

			if( $this->accepts_pm() )
			{ // no email option - try to log in and send private message (only registered users can send PM)
				return 'login';
			}
		}

		// No messaging option between current_User and this user:
		return NULL;
	}


	/**
	 * Get the reason why current User is not able to make a contact with this User
	 *
	 * This is used when get_msgform_possibility returns NULL;
	 *
	 * @return string
	 */
	function get_no_msgform_reason()
	{
		global $current_User;

		if( ( is_logged_in() ) && $this->accepts_pm() )
		{
			if( has_cross_country_restriction( 'contact' ) && ( empty( $current_User->ctry_ID ) || ( $current_User->ctry_ID !== $this->ctry_ID ) ) )
			{ // Contat to this user is not enabled
				return T_( 'You are not allowed to contact with this user.' );
			}
			if( $current_User->accepts_pm() )
			{ // current User accepts private messages
				if( $current_User->ID == $this->ID )
				{ // current User and recipient user are the same
					return T_( 'You cannot send a private message to yourself.' );
				}
				// current User is not able to contact with this User because User has blocked current User or current User is not in this users contact list
				return T_( 'This user can only be contacted through private messages, but you are not allowed to send private message to this user.' );
			}
			// current User has no messaging permission or has disabled private messages in user preferences
			return T_( 'This user can only be contacted through private messages but you are not allowed to send any private messages.' );
		}
		// current User is not logged in or this User doesn't want to be contacted
		return T_( 'This user does not wish to be contacted directly.' );
	}


	/**
	 * Insert object into DB based on previously recorded changes
	 *
	 * Triggers the plugin event AfterUserInsert.
	 *
	 * @param boolean TRUE to automatically create new blog if group has permission
	 * @return boolean true on success
	 */
	function dbinsert( $create_auto_blog = true )
	{
		global $Plugins, $DB, $Settings;

		$DB->begin();

		if( $result = parent::dbinsert() )
		{ // We could insert the user object..

			// Save secondary groups:
			$this->update_secondary_groups();

			// Add new fields:
			if( !empty($this->new_fields) )
			{
				$sql = 'INSERT INTO T_users__fields( uf_user_ID, uf_ufdf_ID, uf_varchar )
								VALUES ('.$this->ID.', '.implode( '), ('.$this->ID.', ', $this->new_fields ).' )';
				$DB->query( $sql, 'Insert new fields' );
				// Reset new fields in object:
				$this->new_fields = array();
			}

			// NEWSLETTERS:
			$insert_newsletters = array();
			if( isset( $Settings ) && ( ! isset( $this->insert_default_newsletters ) || $this->insert_default_newsletters ) )
			{	// Insert default newsletter subscriptions for this user,
				// Only when general settings are defined (to avoid error on install process),
				// And if it is not disabled for exmaple by widget "Email capture / Quick registration",
				$insert_newsletters = ( $Settings->get( 'def_newsletters' ) == '' ? array() : explode( ',', trim( $Settings->get( 'def_newsletters' ), ',' ) ) );
			}
			if( ! empty( $this->new_newsletter_subscriptions ) )
			{	// Also insert additional newsletters(e-g from widget "Email capture / Quick registration"):
				$insert_newsletters = array_merge( $insert_newsletters, $this->new_newsletter_subscriptions );
			}
			if( ! empty( $insert_newsletters ) )
			{	// Do subscribing if at least one newsletter is selected by default:
				$this->subscribe( array_unique( $insert_newsletters ) );
			}

			// USER TAGS:
			if( $result )
			{ // Let's handle the tags
				$this->insert_update_user_tags( 'insert' );
			}

			// Notify plugins:
			// A user could be created also in another DB (to synchronize it with b2evo)
			$Plugins->trigger_event( 'AfterUserInsert', $params = array( 'User' => & $this ) );

			$Group = & $this->get_Group();
			if( $create_auto_blog && $Group->check_perm( 'perm_getblog', 'allowed' ) )
			{ // automatically create new blog for this user
				// TODO: sam2kb> Create a blog only when this user is validated!
				$new_Blog = new Blog( NULL );
				$shortname = $this->get( 'login' );
				$new_Blog->set( 'sec_ID', $Group->get_setting( 'perm_default_sec_ID' ) );
				$new_Blog->set( 'owner_user_ID', $this->ID );
				$new_Blog->set( 'shortname', $shortname );
				$new_Blog->set( 'name', $shortname.'\'s blog' );
				$new_Blog->set( 'locale', $this->get( 'locale' ));
				$new_Blog->set( 'urlname', urltitle_validate( $shortname, $shortname, $new_Blog->ID, false, 'blog_urlname', 'blog_ID', 'T_blogs', $this->get( 'locale' ) ) );

				// Defines blog settings by its kind.
				$Plugins->trigger_event( 'InitCollectionKinds', array(
								'Blog' => & $new_Blog,
								'kind' => 'std',
							) );

				$new_Blog->create();

				// Don't show a sample collection on top menu in back-office:
				// TODO: In another branch Erwin has implemented a rule similar to "only enable first 10 collections". This will be merged here at some point.
				$new_Blog->favorite( NULL, 0 );
			}

			// Save IP Range and user counter
			antispam_increase_counter( 'user' );
		}

		$DB->commit();

		return $result;
	}


	/**
	 * Update the DB based on previously recorded changes.
	 *
	 * Triggers the plugin event AfterUserUpdate.
	 */
	function dbupdate()
	{
		global $DB, $Plugins, $localtimenow;

		$DB->begin();

		$result = parent::dbupdate();

		// Save secondary groups:
		$this->update_secondary_groups();

		// Update existing fields:
		if( !empty($this->updated_fields) )
		{
			foreach( $this->updated_fields as $uf_ID => $uf_val )
			{
				// Note the updated_fields key values must be integers, so don't need casting or DB->quote()
				if( empty( $uf_val ) )
				{	// Delete field:
					$DB->query( 'DELETE FROM T_users__fields
														 WHERE uf_ID = '.$uf_ID );
				}
				else
				{	// Update field:
					$DB->query( 'UPDATE T_users__fields
													SET uf_varchar = '.$DB->quote( $uf_val ).'
												WHERE uf_ID = '.$uf_ID );
				}
			}
			// Reset updated fields in object:
			$this->updated_fields = array();
		}

		// Add new fields:
		if( !empty($this->new_fields) )
		{
			$sql = 'INSERT INTO T_users__fields( uf_user_ID, uf_ufdf_ID, uf_varchar )
							VALUES ('.$this->ID.', '.implode( '), ('.$this->ID.', ', $this->new_fields ).' )';
			$DB->query( $sql, 'Insert new fields' );
			// Reset new fields in object:
			$this->new_fields = array();
		}

		// Update newsletter subscriptions:
		if( $this->newsletter_subscriptions_updated )
		{	// Only if they have been changed:
			// Insert newsletter subscriptions for this user:
			$this->subscribe( $this->new_newsletter_subscriptions );
			// Unsubscribe from unchecked newsletters:
			$this->unsubscribe( array_diff( $this->get_newsletter_subscriptions( 'all' ), $this->new_newsletter_subscriptions ) );
		}

		// Update user tags:
		if( isset( $this->dbchanges_flags['user_tags'] ) )
		{ // Let's handle the tags:
			$this->insert_update_user_tags( 'update' );
		}

		// Notify plugins:
		// Example: An authentication plugin could synchronize/update the password of the user.
		$Plugins->trigger_event( 'AfterUserUpdate', $params = array( 'User' => & $this ) );

		if( isset( $this->previous_status ) &&
		    ! in_array( $this->previous_status, array( 'activated', 'manualactivated', 'autoactivated' ) ) &&
		    in_array( $this->get( 'status' ), array( 'activated', 'manualactivated', 'autoactivated' ) ) )
		{	// Complete activation if user has been activated:
			$this->complete_activation();
		}

		$DB->commit();

		// BLOCK CACHE INVALIDATION:
		// This User has been modified, cached content depending on it should be invalidated:
		BlockCache::invalidate_key( 'user_ID', $this->ID );

		return true;
	}


	/**
	 * Delete those users from the database which corresponds to the given condition or to the given ids array
	 * Note: the delete cascade arrays are handled!
	 *
	 * @param string where condition
	 * @param array object ids
	 * @param array additional params if required
	 * @return mixed # of rows affected or false if error
	 */
	static function db_delete_where( $sql_where, $object_ids = NULL, $params = NULL )
	{
		global $DB;

		$DB->begin();

		if( ! empty( $sql_where ) )
		{
			$object_ids = $DB->get_col( 'SELECT user_ID FROM T_users WHERE '.$sql_where );
		}

		if( ! $object_ids )
		{ // There is no user to delete
			$DB->commit();
			return;
		}

		// Delete orphan threads per users
		$result = delete_orphan_threads( $object_ids );

		// Remove deleted user(s) from posts where it was as last edit user
		$user_ids = implode( ',', $object_ids );
		$result = $result && ( $DB->query( 'UPDATE T_items__item
								    SET post_lastedit_user_ID = NULL
								  WHERE post_lastedit_user_ID IN ( '.$user_ids.' )' ) !== false );
		$result = $result && ( $DB->query( 'UPDATE T_items__version
								    SET iver_edit_user_ID = NULL
								  WHERE iver_edit_user_ID IN ( '.$user_ids.' )' ) !== false );

		// Remove this user from links where it was as last edit user
		$result = $result && ( $DB->query( 'UPDATE T_links
								    SET link_lastedit_user_ID = NULL
								  WHERE link_lastedit_user_ID IN ( '.$user_ids.' )' ) !== false );

		if( $result )
		{ // Delete the user(s) with all of the cascade objects
			$params['use_transaction'] = false; // no need to start a new transaction
			$result = parent::db_delete_where( $sql_where, $object_ids, $params );
		}

		if( $result !== false )
		{ // delete the users' media folders recursively, for all deleted users
			$FileRootCache = & get_FileRootCache();
			foreach( $object_ids as $user_ID )
			{
				if( $root_directory = $FileRootCache->get_root_dir( 'user', $user_ID ) )
				{ // Delete the folder only when it is detected:
					rmdir_r( $root_directory );
				}
			}
		}

		( $result !== false ) ? $DB->commit() : $DB->rollback();

		return $result;
	}


	/**
	 * Delete user and dependencies from database
	 *
	 * Includes WAY TOO MANY requests because we try to be compatible with MySQL 3.23, bleh!
	 *
	 * @param Log Log object where output gets added (by reference).
	 */
	function dbdelete( & $Log = array() )
	{
		global $DB, $Plugins, $current_User;

		if( $this->ID == 0 ) debug_die( 'Non persistant object cannot be deleted!' );

		if( $this->ID == 1 ||
		    ( is_logged_in() && $this->ID == $current_User->ID ) )
		{	// Don't allow to delete first admin user and current logged in user:
			return false;
		}

		$deltype = param( 'deltype', 'string', '' ); // spammer

		// Check to delete additional data if we are deleting user as spammer or it is checked on the submitted form:
		$delete_messages = ( $deltype == 'spammer' || param( 'force_delete_messages', 'integer', 0 ) );
		$delete_comments = ( $deltype == 'spammer' || param( 'force_delete_comments', 'integer', 0 ) );
		$delete_files    = ( $deltype == 'spammer' || param( 'force_delete_files', 'integer', 0 ) );

		$DB->begin();

		$this->init_relations( array(
				'delete_messages' => $delete_messages,
				'delete_comments' => $delete_comments,
				'delete_files'    => $delete_files,
			) );

		if( ! $delete_comments )
		{	// Keep user's comments as from anonymous user,
			// Transform registered user comments to unregistered:
			$ret = $DB->query( 'UPDATE T_comments
													SET comment_author_user_ID = NULL,
															comment_author = '.$DB->quote( $this->get('preferredname') ).',
															comment_author_email = '.$DB->quote( $this->get('email') ).',
															comment_author_url = '.$DB->quote( $this->get('url') ).'
													WHERE comment_author_user_ID = '.$this->ID );
			if( $Log instanceof log )
			{
				$Log->add( 'Transforming user\'s comments to unregistered comments... '.sprintf( '(%d rows)', $ret ), 'note' );
			}
		}

		if( ! $delete_files )
		{	// Keep user's files as from anonymous user:
			$ret = $DB->query( 'UPDATE T_files
				  SET file_creator_user_ID = NULL
				WHERE file_creator_user_ID = '.$this->ID. ' AND file_root_type != "user"' );
			$ret = $DB->query( 'UPDATE T_links
				  SET link_creator_user_ID = NULL
				WHERE link_creator_user_ID = '.$this->ID );
			if( $Log instanceof log )
			{
				$Log->add( 'Setting user\'s uploaded files creator ID to NULL...'.sprintf( '(%d rows)', $ret ), 'note' );
			}
		}

		// remember ID, because parent method resets it to 0
		$old_ID = $this->ID;
		$old_email = $this->get( 'email' );

		// Delete main object:
		if( ! parent::dbdelete() )
		{
			$DB->rollback();

			$Log->add( 'User has not been deleted.', 'error' );
			return false;
		}

		if( $deltype == 'spammer' )
		{ // User was deleted as spammer, we should mark email of this user as 'Spammer'
			$EmailAddressCache = & get_EmailAddressCache();
			$EmailAddress = & $EmailAddressCache->get_by_name( $old_email, false, false );
			if( !$EmailAddress )
			{	// Create new record in the T_email_address table
				$EmailAddress = new EmailAddress();
				$EmailAddress->set( 'address', $old_email );
			}
			if( !empty( $EmailAddress ) )
			{ // Save status of an email address
				$EmailAddress->set( 'status', 'spammer' );
				$EmailAddress->dbsave();
			}
		}

		$DB->commit();

		if( $Log instanceof log )
		{
			$Log->add( 'Deleted User.', 'note' );
		}

		// Notify plugins:
		$this->ID = $old_ID;
		$Plugins->trigger_event( 'AfterUserDelete', $params = array( 'User' => & $this ) );
		$this->ID = 0;

		// BLOCK CACHE INVALIDATION:
		// This User has been modified, cached content depending on it should be invalidated:
		BlockCache::invalidate_key( 'user_ID', $old_ID );

		return true;
	}


	/**
	 * Send an email to the user with a link to validate/confirm his email address.
	 *
	 * If the email could get sent, it saves the used "request_id" into the user's Session.
	 *
	 * @param string URL, where to redirect the user after he clicked the validation link (gets saved in Session).
	 * @param integer Collection ID
	 * @param boolean TRUE if user email is changed
	 * @return boolean True, if the email could get sent; false if not
	 */
	function send_validate_email( $redirect_to_after, $blog = NULL, $email_changed = false )
	{
		global $app_name, $Session, $baseurl, $servertimenow;
		global $Settings, $UserSettings;

		if( ! empty( $this->welcome_activate_email_campaign_sent ) )
		{	// This flag means we don't need to send a separate activation email
			// because at least one special(Welcome-Activate) email campaign was sent
			// on auto newsletter subscribing before calling this function.
			// Return TRUE in order to display the same info messages what user sees on normal activation,
			// because Welcome-Activate email campaign may contains button/link to activate user account:
			return true;
		}

		// Display messages depending on user email status
		display_user_email_status_message( $this->ID );

		if( empty( $redirect_to_after ) )
		{	// If redirect to was not set:
			$redirect_to_after = param( 'redirect_to', 'url', '' );
			if( empty( $redirect_to_after ) )
			{
				if( is_admin_page() )
				{
					$redirect_to_after = regenerate_url( 'action' );
				}
				else
				{
					$redirect_to_after = $this->get_userpage_url();
				}
			}
		}

		if( $Settings->get( 'validation_process' ) == 'easy' )
		{	// Validation process is set to easy, send and easy activation email:
			return send_easy_validate_emails( array( $this->ID ), false, $email_changed, $redirect_to_after );
		}

		// Secure validation process:

		if( mail_is_blocked( $this->email ) )
		{ // prevent trying to send an email to a blocked email address ( Note this is checked in the send_easy_validate_emails too )
			return false;
		}

		$request_id = generate_random_key(22);

		$blog_param = ( empty( $blog ) ) ? '' : '&inskin=1&blog='.$blog;

		// Change locale here to localize the email subject and content
		locale_temp_switch( $this->get( 'locale' ) );
		$email_template_params = array(
				'status'     => $this->status,
				'blog_param' => $blog_param,
				'request_id' => $request_id,
			);
		$r = send_mail_to_User( $this->ID, sprintf( T_( 'Activate your account: %s' ), '$login$' ), 'account_activate', $email_template_params, true );
		locale_restore_previous();

		if( $r )
		{ // save request_id into Session
			$request_ids = $Session->get( 'core.activateacc.request_ids' );
			if( ( ! is_array($request_ids) ) || $email_changed )
			{ // create new request ids array if it doesn't exist yet, or if user email changed ( this way the old request into the old email address won't be valid )
				$request_ids = array();
			}
			$request_ids[] = $request_id;
			$Session->set( 'core.activateacc.request_ids', $request_ids, 86400 * 2 ); // expires in two days (or when clicked)
			// set a redirect_to session variable because this way after the account will be activated we will know where to redirect
			$Session->set( 'core.activateacc.redirect_to', $redirect_to_after  );
			$Session->dbsave(); // save immediately

			// update last activation email timestamp
			$UserSettings->set( 'last_activation_email', date2mysql( $servertimenow ), $this->ID );
			$UserSettings->dbupdate();
		}

		return $r;
	}


	/**
	 * Activate user account after user clicks on activate link from email
	 */
	function activate_from_Request()
	{
		// Activate current user account:
		$this->set( 'status', 'activated' );
		$this->dbupdate();
	}


	/**
	 * Do actions to complete activation for this user account
	 */
	function complete_activation()
	{
		global $DB, $Settings, $UserSettings, $current_User;

		if( empty( $UserSettings ) )
		{	// initialize UserSettings:
			load_class( 'users/model/_usersettings.class.php', 'UserSettings' );
			$UserSettings = new UserSettings();
		}

		// Clear last reminder key and last activation email date because the user was activated:
		$UserSettings->delete( 'last_activation_reminder_key', $this->ID );
		$UserSettings->delete( 'last_activation_email', $this->ID );
		$UserSettings->delete( 'activation_reminder_count', $this->ID );
		$UserSettings->delete( 'send_activation_reminder', $this->ID );
		$UserSettings->dbupdate();

		if( $Settings->get( 'newusers_findcomments' ) )
		{	// We have to assign the all old comments from current user by email:
			$DB->query( 'UPDATE T_comments
				   SET comment_author_user_ID = '.$DB->quote( $this->ID ).'
				 WHERE comment_author_email = '.$DB->quote( $this->email ).'
				   AND comment_author_user_ID IS NULL',
				'Assign anonymous comments to register user after activation' );
			// Subscribe user to posts where he selected "Notify me of replies" on creating those anonymous comments:
			$SQL = new SQL();
			$SQL->SELECT( 'DISTINCT comment_item_ID, comment_author_user_ID, 1' );
			$SQL->FROM( 'T_comments' );
			$SQL->WHERE( 'comment_author_user_ID = '.$DB->quote( $this->ID ) );
			$SQL->WHERE_and( 'comment_anon_notify = 1' );
			$DB->query( 'REPLACE INTO T_items__subscriptions ( isub_item_ID, isub_user_ID, isub_comments ) '.$SQL->get(),
				'Subscribe user to posts where he selected "Notify me of replies" on creating those anonymous comments' );
		}

		// Create a welcome private message when user's status was changed to Active:
		$this->send_welcome_message();

		// Send notification email about activated account to users with edit users permission:
		send_admin_notification( NT_('New user account activated'), 'account_activated', array(
				'User' => $this,
				'login' => $this->login, // this is required in the send_admin_notification
				// If admin activated some other user account:
				'activated_by_admin' => ( is_logged_in() && $current_User->ID != $this->ID ? $current_User->get_username() : '' ),
			) );
	}


	// Template functions {{{

	/**
	 * Template function: display user's level
	 */
	function level()
	{
		$this->disp( 'level', 'raw' );
	}


	/**
	 * Template function: display user's login
	 *
	 * @param string Output format, see {@link format_to_output()}
	 */
	function login( $format = 'htmlbody' )
	{
		$this->disp( 'login', $format );
	}


	/**
	 * Template helper function: Get a link to a message form for this 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
	 */
	function get_msgform_link( $form_url = NULL, $before = ' ', $after = ' ', $text = '#', $title = '#', $class = '' )
	{
		if( empty($this->email) )
		{ // We have no email for this User :(
			return false;
		}

		$available_msgform = $this->get_msgform_possibility();
		if( ! $available_msgform )
		{	// There is no way this user accepts receiving messages.
			return false;
		}

		if( is_null($form_url) )
		{
			global $Collection, $Blog;
			$form_url = isset($Blog) ? $Blog->get('msgformurl') : '';
		}

		if( param( 'redirect_to', 'url', NULL ) !== NULL )
		{	// Use current redirect URL:
			$redirect_to = get_param( 'redirect_to' );
		}
		else
		{	// Generate new redirect URL:
			$redirect_to = regenerate_url( '', '', '', '&' );
		}

		$form_url = url_add_param( $form_url, 'recipient_id='.$this->ID.'&amp;redirect_to='.rawurlencode( url_rel_to_same_host( $redirect_to, $form_url ) ) );

		if( $title == '#' )
		{
			switch( $available_msgform )
			{
				case 'email':
					$title = T_('Send email to user');
					break;
				case 'PM':
				case 'login':
				default:
					$title = T_('Send message to user');
					break;
			}
		}
		if( $text == '#' ) $text = get_icon( 'email', 'imgtag', array( 'class' => 'middle', 'title' => $title ) );

		$r = '';
		$r .= $before;
		$r .= '<a href="'.$form_url.'" title="'.$title.'"';
		if( !empty( $class ) )
		{
			$r .= ' class="'.$class.'"';
		}
		$r .= '>'.$text.'</a>';
		$r .= $after;

		return $r;
	}


	/**
	 * Template function: display a link to a message form for this 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
	 */
	function msgform_link( $form_url = NULL, $before = ' ', $after = ' ', $text = '#', $title = '#', $class = '' )
	{
		echo $this->get_msgform_link( $form_url, $before, $after, $text, $title, $class );
	}


	/**
	 * Template function: display user's preferred name
	 *
	 * @param string Output format, see {@link format_to_output()}
	 */
	function preferred_name( $format = 'htmlbody' )
	{
		echo format_to_output( $this->get_preferred_name(), $format );
	}


	/**
	 * Template function: display user's URL
	 *
	 * @param string string to display before the date (if changed)
	 * @param string string to display after the date (if changed)
	 * @param string Output format, see {@link format_to_output()}
	 */
	function url( $before = '', $after = '', $format = 'htmlbody' )
	{
		if( !empty( $this->url ) )
		{
			echo $before;
			$this->disp( 'url', $format );
			echo $after;
		}
	}


	/**
	 * Template function: display number of user's posts
	 */
	function num_posts( $format = 'htmlbody' )
	{
		echo format_to_output( $this->get_num_posts(), $format );
	}


	/**
	 * Template function: display first name of the user
	 */
	function first_name( $format = 'htmlbody' )
	{
		$this->disp( 'firstname', $format );
	}


	/**
	 * Template function: display last name of the user
	 */
	function last_name( $format = 'htmlbody' )
	{
		$this->disp( 'lastname', $format );
	}


	/**
	 * Template function: display nickname of the user
	 */
	function nick_name( $format = 'htmlbody' )
	{
		$this->disp( 'nickname', $format );
	}


	/**
	 * Return gender of the user
	 */
	function get_gender()
	{
		switch( $this->gender )
		{
			case 'M':
				return T_('A man');

			case 'F':
				return T_('A woman');

			case 'O':
				return T_('Other');
		}

		return NULL;
	}


	/**
	 * Return attr class depending on gender of the user
	 */
	function get_gender_class()
	{
		$gender_class = 'user';

		if( $this->check_status( 'is_closed' ) )
		{ // Set different gender color if user is closed
			return 'user closed';
		}

		if( ! check_setting( 'gender_colored' ) )
		{ // Don't set gender color if setting is OFF
			return $gender_class;
		}

		switch( $this->gender )
		{ // Set a class name for each gender type
			case 'M':
				$gender_class .= ' man';
				break;
			case 'F':
				$gender_class .= ' woman';
				break;
			case 'O':
				$gender_class .= ' other';
				break;
			default:
				$gender_class .= ' nogender';
				break;
		}

		return $gender_class;
	}


	/**
	 * Template function: display email of the user
	 */
	function email( $format = 'htmlbody' )
	{
		$this->disp( 'email', $format );
	}


	/**
	 * Template function: display ICQ of the user
	 * @deprecated
	 */
	function icq( $format = 'htmlbody' )
	{
	}


	/**
	 * Template function: display AIM of the user.
	 * @deprecated
	 */
	function aim( $format = 'htmlbody' )
	{
	}


	/**
	 * Template function: display Yahoo IM of the user
	 * @deprecated
	 */
	function yim( $format = 'htmlbody' )
	{
	}


	/**
	 * Template function: display MSN of the user
	 * @deprecated
	 */
	function msn( $format = 'htmlbody' )
	{
	}

	// }}}


	function has_avatar()
	{
		global $Settings;

		return ( !empty( $this->avatar_file_ID ) && $Settings->get('allow_avatars') );
	}


	/**
	 * Get {@link File} object of the user's avatar.
	 *
	 * @return File This may be NULL.
	 */
	function & get_avatar_File()
	{
		$File = NULL;

		if( $this->has_avatar() )
		{
			$FileCache = & get_FileCache();

			// Do not halt on error. A file can disappear without the profile being updated.
			/**
			 * @var File
			 */
			$File = & $FileCache->get_by_ID( $this->avatar_file_ID, false, false );
		}

		return $File;
	}


	/**
	 * Get {@link Link} object of the user's avatar.
	 *
	 * @return Link This may be NULL.
	 */
	function & get_avatar_Link()
	{
		$Link = NULL;

		if( $this->has_avatar() )
		{
			$LinkOwner = new LinkUser( $this );
			$Link = $LinkOwner->get_link_by_file_ID( $this->avatar_file_ID );
		}

		return $Link;
	}


	/**
	 * Get array of {@link Link} objects of the previously uploaded avatars.
	 *
	 * @param boolean TRUE - to exclude main picture from list
	 * @param boolean TRUE - to ignore the settings and get the avatars anyway
	 * @return array of Links
	 */
	function get_avatar_Links( $exclude_main_picture = true, $ignore_settings = false )
	{
		global $Settings;

		$avatar_Links = array();

		if( !$ignore_settings && !( $Settings->get('upload_enabled') && $Settings->get( 'fm_enable_roots_user' ) ) )
		{ // Upload is not enabled and we have no permission to use it...
			return $avatar_Links;
		}

		$LinkOwner = new LinkUser( $this );
		foreach( $LinkOwner->get_Links() as $user_Link )
		{
			$l_File = & $user_Link->get_File();
			if( ! empty( $l_File ) && $l_File->is_image() )
			{
				if( $exclude_main_picture && $l_File->ID == $this->avatar_file_ID )
				{ // Exclude the main picture from list of other pictures
					continue;
				}
				$avatar_Links[] = $user_Link;
			}
		}

		return $avatar_Links;
	}


	/**
	 * Get avatar IMG tag.
	 *
	 * @param string size
	 * @param string class
	 * @param string align
	 * @param boolean true if the avatar image should be zoomed on click, false otherwise
	 * @param string avatar overlay text
	 * @param string group name for lightbox plugin
	 * @param string Change size of the attributes "width" & "height".
	 *               Example: ( $tag_size = '160' ) => width="160" height="160"
	 *                        ( $tag_size = '160x320' ) => width="160" height="320"
	 *                        NULL - use real size
	 * @param string Protocol is used for gravatar, example: 'http:' or 'https:'
	 * @return string
	 */
	function get_avatar_imgtag( $size = 'crop-top-64x64', $class = 'avatar', $align = '', $zoomable = false, $avatar_overlay_text = '', $lightbox_group = '', $tag_size = NULL, $protocol = '' )
	{
		/**
		 * @var Link
		 */
		$Link = & $this->get_avatar_Link();

		/**
		 * @var File
		 */
		if( ! $Link || ! $File = & $Link->get_File() )
		{ // User doesn't have an avatar
			return get_avatar_imgtag_default( $size, $class, $align, array(
					'email'    => $this->get( 'email' ),
					'gender'   => $this->get( 'gender' ),
					'tag_size' => $tag_size,
					'protocol' => $protocol,
				) );
		}

		if( ! $this->check_status( 'can_display_avatar' ) && ! ( is_admin_page() && check_user_perm( 'users', 'edit', false, NULL, false ) ) )
		{ // if the user status doesn't allow to display avatar and current User is not an admin in admin interface, then show default avatar
			return get_avatar_imgtag_default( $size, $class, $align, array(
					'email'    => $this->get( 'email' ),
					'gender'   => $this->get( 'gender' ),
					'tag_size' => $tag_size,
					'protocol' => $protocol,
				) );
		}

		if( $zoomable )
		{ // return clickable avatar tag, zoom on click
			if( is_logged_in() )
			{ // Only logged in users can see a big picture of the avatar
				// set random value to link_rel, this way the pictures on the page won't be grouped
				// this is usefull because the same avatar picture may appear more times in the same page
				if( empty( $lightbox_group ) )
				{
					$link_rel = 'lightbox[l'.$Link->ID.rand(0, 100000).']';
				}
				else
				{
					$link_rel = 'lightbox['.$lightbox_group.']';
				}
				$r = $Link->get_tag( array(
						'before_image'        => '',
						'before_image_legend' => '',
						'after_image_legend'  => '',
						'after_image'         => '',
						'image_size'          => $size,
						'image_link_title'    => $this->get_username(),
						'image_link_rel'      => $link_rel,
						'image_class'         => $class,
						'image_align'         => $align,
						'tag_size'            => $tag_size,
					) );
			}
			else
			{ // Anonymous user get an avatar picture with link to login page
				global $Collection, $Blog;
				$redirect_to = '';
				if( isset( $Blog ) )
				{ // Redirect user after login
					$redirect_to = $Blog->get( 'userurl', array( 'user_ID' => $this->ID, 'user_login' => $this->login ) );
				}
				$r = '<a href="'.get_login_url( 'cannot see avatar', $redirect_to ) .'">'.$File->get_thumb_imgtag( $size, $class, $align, '', $tag_size ).'</a>';
			}
		}
		else
		{
			$r = $File->get_thumb_imgtag( $size, $class, $align, '', $tag_size );
		}

		if( $r != '' && $avatar_overlay_text != '' )
		{ // Add overlay text if it is enabled
			$r = $this->get_avatar_overlay_text( $r, $size, $avatar_overlay_text, $class, $File );
		}

		return $r;
	}


	/**
	 * Get overlay text for avatar img tag
	 *
	 * @param string Tag <img />
	 * @param array Avatar size, keys: 'width' & 'height'
	 * @param string Avatar overlay text
	 * @param string Class
	 * @param object File
	 * @return string Tag <img /> with overlay text
	 */
	function get_avatar_overlay_text( $img_tag, $thumb_size, $avatar_overlay_text, $class = '', $File = NULL )
	{
		preg_match( '/ width="(\d+)" height="(\d+)" /i', $img_tag, $img_sizes );
		if( count( $img_sizes ) == 3 )
		{ // img tag has a defined width & height
			$width = $img_sizes[1];
			$height = $img_sizes[2];
		}
		else
		{ // We try to get a sizes from config
			global $thumbnail_sizes;
			if( isset( $thumbnail_sizes[ $thumb_size ] ) )
			{ // Set a sizes
				if( ! is_null( $File ) && $thumb_sizes = $File->get_thumb_size( $thumb_size ) )
				{ // Get sizes that are used for thumbnail really
					list( $width, $height ) = $thumb_sizes;
				}
				else
				{ // Use the size of thumbnail config if File is not defined
					$width = $thumbnail_sizes[ $thumb_size ][1];
					$height = $thumbnail_sizes[ $thumb_size ][2];
				}
			}
		}

		if( empty( $width ) || empty( $height ) )
		{ // If sizes is not defined we cannot calculate a font-size for an overlay text
			return $img_tag;
		}

		$overlay_lines = explode( "\n", str_replace( "\r", '', $avatar_overlay_text ) );
		$max_line_length = 0;
		foreach( $overlay_lines as $line )
		{ // Find the most long line of the overlay text
			if( $max_line_length < strlen($line) )
			{	// Get max long line
				$max_line_length = strlen($line);
			}
		}
		if( $max_line_length > 0 )
		{ // Display an overlay text if max length is defined
			// Calculate approximate font size, 1.7 - is custom coefficient of the font
			$font_size = ceil( ( $width / $max_line_length ) * 1.7 );
			// Set line-height for centering text by vertical
			$line_height = ceil( ( $height / count( $overlay_lines ) ) * 0.82 );
			// Padding-top give us a vertical centering
			$padding_top = ceil( $line_height * 0.32 );

			$tag_is_linked = false;
			if( strpos( $img_tag, '</a>' ) !== false )
			{ // img_tag is located inside tag <a>, we should to remove a end of the tag and then add
				$img_tag = str_replace( '</a>', '', $img_tag );
				$tag_is_linked = true;
			}
			$img_tag = '<div class="bubletip_overlay_text '.$class.'">'.
					$img_tag.
					'<div style="font-size:'.$font_size.'px;line-height:'.$line_height.'px;padding-top:'.$padding_top.'px">'.
						nl2br($avatar_overlay_text).
					'</div>';
			if( $tag_is_linked )
			{ // Add end of the tag which is removed above
				$img_tag .= '</a>';
			}
			$img_tag .= '</div>';
		}

		return $img_tag;
	}


	/**
	 * Get styled avatar
	 *
	 * @param array params
	 * @return string
	 */
	function get_avatar_styled( $params = array() )
	{
		global $thumbnail_sizes;

		$params = array_merge( array(
				'block_class'  => 'avatar_rounded',
				'size'         => 'crop-top-64x64',
				'avatar_class' => 'avatar',
				'zoomable'     => false,
				'overlay_text' => '',
				'show_login'   => true,
				'bubbletip'    => true,
			), $params );

		$bubbletip_param = '';
		if( $params['bubbletip'] )
		{	// Init bubbletip param
			$bubbletip_param = 'rel="bubbletip_user_'.$this->ID.'"';
		}

		$style_width = '';
		if( isset( $thumbnail_sizes[$params['size']] ) )
		{
			$style_width = ' style="width:'.$thumbnail_sizes[$params['size']][1].'px"';
		}

		$identity_url = get_user_identity_url( $this->ID );

		if( !empty( $identity_url ) )
		{
			$r = '<a href="'.$identity_url.'" class="'.$params['block_class'].'"'.$bubbletip_param.$style_width.'>';
		}
		else
		{
			$r = '<div class="'.$params['block_class'].'"'.$bubbletip_param.$style_width.'>';
		}

		$r .= $this->get_avatar_imgtag( $params['size'], $params['avatar_class'], '', $params['zoomable'], $params['overlay_text'] );

		if( $params['show_login'] )
		{	// Display user name
			$r .= $this->get_colored_login( array( 'login_text' => 'name' ) );
		}

		$r .= !empty( $identity_url ) ? '</a>' : '</div>';

		return $r;
	}


	/**
	 * Add a user field
	 *
	 * @param integer User field definition ID
	 * @param string Field value
	 */
	function userfield_add( $uf_ufdf_ID, $val )
	{
		global $DB;
		$this->new_fields[] = $uf_ufdf_ID.', '.$DB->quote( $val );
	}


	/**
	 * Update an user field. Empty fields will be deleted on dbupdate.
	 *
	 * @param integer User field ID
	 * @param string Field value
	 */
	function userfield_update( $uf_ID, $val )
	{
		$this->updated_fields[ $uf_ID ] = $val;
	}


	/**
	 * Get all user extra fields with selected type
	 *
	 * @param string Field type: 'email', 'word', 'number', 'phone', 'text'
	 * @param boolean TRUE to get only the fields with icon
	 * @return array Fields
	 */
	function userfields_by_type( $field_type, $only_with_icon = true )
	{
		global $DB;

		$SQL = new SQL( 'Get values of user fields by type "'.$field_type.'" for User #'.$this->ID );
		$SQL->SELECT( 'uf_varchar, ufdf_ID, ufdf_icon_name, ufdf_code' );
		$SQL->FROM( 'T_users__fields' );
		$SQL->FROM_add( 'INNER JOIN T_users__fielddefs ON uf_ufdf_ID = ufdf_ID' );
		$SQL->WHERE( 'uf_user_ID = '.$this->ID );
		$SQL->WHERE_and( 'ufdf_type = '.$DB->quote( $field_type ) );
		if( $only_with_icon )
		{
			$SQL->WHERE_and( 'ufdf_icon_name IS NOT NULL' );
		}
		$SQL->ORDER_BY( 'ufdf_order' );

		return $DB->get_results( $SQL );
	}


	/**
	 * Get user fields by field definition ID
	 *
	 * @param integer Field definition ID
	 * @return array|false Fields
	 */
	function userfields_by_ID( $ufdf_ID )
	{
		// Load all user fields once:
		$this->userfields_load();

		if( ! empty( $this->userfields_by_type[ $ufdf_ID ] ) )
		{	// Get user fields from cache:
			$userfields = array();
			foreach( $this->userfields_by_type[ $ufdf_ID ] as $uf_ID )
			{
				if( ! empty( $this->userfields[ $uf_ID ] ) )
				{
					$userfields[] = $this->userfields[ $uf_ID ];
				}
			}
			return $userfields;
		}

		// No fields:
		return false;
	}


	/**
	 * Get user field value by ID
	 *
	 * @param integer Field ID
	 * @param boolean TRUE to prepare user field for correct html displaying
	 * @return string Field value
	 */
	function userfield_value_by_ID( $field_ID, $prepare_userfield = true )
	{
		// Load all user fields once:
		$this->userfields_load();

		if( isset( $this->userfields_by_type[ $field_ID ], $this->userfields[ $this->userfields_by_type[ $field_ID ][0] ] ) )
		{	// Get value from cache:
			$userfield = $this->userfields[ $this->userfields_by_type[ $field_ID ][0] ];

			if( $prepare_userfield )
			{	// Prepare user field for correct html displaying:
				userfield_prepare( $userfield );
			}

			return $userfield->uf_varchar;
		}

		// No field value
		return '';
	}


	/**
	 * Get user field values by code
	 *
	 * @param integer Field code
	 * @param boolean TRUE to prepare user field for correct html displaying
	 * @return array Field values
	 */
	function userfield_values_by_code( $field_code, $prepare_userfield = true )
	{
		global $DB;

		// Load all user fields once
		$this->userfields_load();

		$field_values = array();
		if( ! empty( $this->userfields_by_code[ $field_code ] ) )
		{ // Get value from cache
			foreach( $this->userfields_by_code[ $field_code ] as $userfield_ID )
			{
				$userfield = $this->userfields[ $userfield_ID ];
				if( $prepare_userfield )
				{	// Prepare user field for correct html displaying:
					userfield_prepare( $userfield );
				}
				$field_values[] = $userfield->uf_varchar;
			}
		}

		return $field_values;
	}


	/**
	 * Load userfields
	 */
	function userfields_load()
	{
		if( $this->userfields_loaded )
		{ // The user fields already were loaded, Don't call DB query twice
			return;
		}

		global $DB;

		$SQL = new SQL( 'Load values of user fields for User #'.$this->ID );
		$SQL->SELECT( 'uf_ID, uf_varchar, ufgp_ID, ufgp_name, T_users__fielddefs.*' );
		$SQL->FROM( 'T_users__fields' );
		$SQL->FROM_add( 'INNER JOIN T_users__fielddefs ON uf_ufdf_ID = ufdf_ID' );
		$SQL->FROM_add( 'INNER JOIN T_users__fieldgroups ON ufdf_ufgp_ID = ufgp_ID' );
		$SQL->WHERE( 'uf_user_ID = '.$this->ID );
		$SQL->WHERE_and( 'ufdf_required != "hidden"' );
		$SQL->ORDER_BY( 'ufgp_order, ufdf_order, uf_ID' );
		$userfields = $DB->get_results( $SQL );

		$userfield_lists = array();
		foreach( $userfields as $u => $userfield )
		{
			if( ! userfield_is_viewable( $this->ID, $userfield->ufdf_visibility, $userfield->ufdf_type ) )
			{	// Current user cannot view this user field:
				unset( $userfields[ $u ] );
				continue;
			}

			if( $userfield->ufdf_duplicated == 'list' )
			{ // Prepare a values of list into one array
				if( !isset( $userfield_lists[$userfield->ufdf_ID] ) )
				{	// Init array
					$userfield_lists[$userfield->ufdf_ID] = array();
				}
				$userfield_lists[$userfield->ufdf_ID][$userfield->uf_ID] = $userfield->uf_varchar;
			}
		}

		foreach( $userfields as $userfield )
		{
			if( $userfield->ufdf_duplicated == 'list' )
			{ // List style
				if( isset( $userfield_lists[$userfield->ufdf_ID] ) )
				{ // Save all data for this field:
					$userfield->list = $userfield_lists[ $userfield->ufdf_ID ];
					$userfield->uf_varchar = implode( ', ', $userfield_lists[$userfield->ufdf_ID] );
					$this->userfields[$userfield->uf_ID] = $userfield;
					$this->userfields_by_code[$userfield->ufdf_code][] = $userfield->uf_ID;
					// Unset array to avoid a duplicates
					unset( $userfield_lists[$userfield->ufdf_ID] );
				}
			}
			else
			{ // Save all data for this field:
				$this->userfields[$userfield->uf_ID] = $userfield;
				$this->userfields_by_code[$userfield->ufdf_code][] = $userfield->uf_ID;
			}
			// Save index
			$this->userfields_by_type[$userfield->ufdf_ID][] = $userfield->uf_ID;
		}

		// Also make sure the definitions are loaded
		$this->userfield_defs_load();

		// Set flag to don't call this function twice:
		$this->userfields_loaded = true;
	}


	/**
	 * Load userfields defs
	 */
	function userfield_defs_load()
	{
		global $DB;

		if( ! isset( $this->userfield_defs ) )
		{
			$SQL = new SQL( 'Load user field definitions' );
			$SQL->SELECT( 'ufdf_ID, ufdf_type, ufdf_name, ufdf_required, ufdf_options, ufdf_duplicated, ufdf_visibility' );
			$SQL->FROM( 'T_users__fielddefs' );
			$userfield_defs = $DB->get_results( $SQL );

			foreach( $userfield_defs as $userfield_def )
			{
				$this->userfield_defs[$userfield_def->ufdf_ID] = array(
					$userfield_def->ufdf_type,
					$userfield_def->ufdf_name,
					$userfield_def->ufdf_required,
					$userfield_def->ufdf_options,
					$userfield_def->ufdf_duplicated,
					$userfield_def->ufdf_visibility,
				); //jamesz
			}
		}
	}


	/**
	 * Get first field for a specific type
	 *
	 * @param integer Field type ID
	 * @param boolean TRUE to prepare user field for correct html displaying
	 * @return string or NULL
	 */
	function userfieldget_first_for_type( $type_ID, $prepare_userfield = true )
	{
		$this->userfields_load();

		if( ! isset( $this->userfields_by_type[ $type_ID ] ) )
		{
			return NULL;
		}

		$idx = $this->userfields_by_type[ $type_ID ][0];

		$userfield = $this->userfields[ $idx ];

		if( $prepare_userfield )
		{	// Prepare user field for correct html displaying:
			userfield_prepare( $userfield );
		}

		return $userfield->uf_varchar;
	}


	/**
	 * Update user data from Request form fields.
	 *
	 * @param boolean is new user
	 * @return mixed true on success, allowed action otherwise
	 */
	function update_from_request( $is_new_user = false )
	{
		global $current_User, $DB, $Messages, $UserSettings, $Settings, $blog, $admin_url;
		global $is_api_request;

		if( ! $current_User->can_moderate_user( $this->ID ) && $this->ID != $current_User->ID )
		{ // user is only allowed to update him/herself
			$Messages->add( T_('You are only allowed to update your own profile!') );
			return 'view';
		}

		// check if is admin form
		$is_admin_form = param( 'admin_form', 'boolean', false );

		// check if is a register finish form:
		$is_register_finish_form = ( param( 'user_tab', 'string', '' ) == 'register_finish' );

		// memorize user status ( activated or not )
		$user_was_activated = $this->check_status( 'is_validated' );
		$user_old_email = $this->email;

		// memorize user old login, status and root path, before update
		$user_old_login = $this->login;
		$user_root_path = NULL;
		$FileRootCache = & get_FileRootCache();
		if( !$is_new_user )
		{
			$user_FileRoot = & $FileRootCache->get_by_type_and_ID( 'user', $this->ID );
			if( $user_FileRoot && file_exists( $user_FileRoot->ads_path ) )
			{
				$user_root_path = $user_FileRoot->ads_path;
			}
		}

		if( $is_register_finish_form )
		{	// load data from register form:
			$load_result = $this->load_from_register_form();
		}
		else
		{	// load data from request:
			$load_result = $this->load_from_Request();
		}


		if( ! $load_result )
		{	// We have found validation errors:
			if( $is_new_user || ( $is_admin_form && ( $this->ID != 1 ) ) )
			{ // update user status settings but not save
				$this->update_status_from_Request( false );
			}
			return 'edit';
		}

		// Update user
		$DB->begin();

		$is_password_form = param( 'password_form', 'boolean', false );
		if( $this->dbsave() )
		{
			$update_success = true;
			if( $is_new_user )
			{
				// Find and inform administrator about other users with same email address on creating new user from back-office:
				$SQL = new SQL( 'Find other users with same email as new created user' );
				$SQL->SELECT( 'user_login' );
				$SQL->FROM( 'T_users' );
				$SQL->WHERE( 'user_email = '.$DB->quote( $this->get( 'email' ) ) );
				$SQL->WHERE_and( 'user_ID != '.$DB->quote( $this->ID ) );
				$same_email_logins = $DB->get_col( $SQL );
				if( ! empty( $same_email_logins ) )
				{	// If at least one user exists with same email address:
					foreach( $same_email_logins as $l => $same_email_login )
					{	// Display a login as link to account profile page:
						$same_email_logins[ $l ] = get_user_identity_link( $same_email_login );
					}
					$Messages->add( sprintf( T_('WARNING: the user was created but it shares the same email address as: %s.'), implode( ', ', $same_email_logins ) ), 'warning' );
				}

				$Messages->add( T_('New user has been created.'), 'success' );
				report_user_create( $this );
			}
			elseif( $is_password_form )
			{
				$Messages->add( T_('Password has been changed.'), 'success' );
			}
			else
			{
				if( $user_old_login != $this->login && $user_root_path != NULL )
				{ // user login changed and user has a root directory (another way $user_root_path value would be NULL)
					$FileRootCache->clear();
					$user_FileRoot = & $FileRootCache->get_by_type_and_ID( 'user', $this->ID );
					if( $user_FileRoot )
					{ // user FilerRooot exists, rename user root folder
						if( ! @rename( $user_root_path, $user_FileRoot->ads_path ) )
						{ // unsuccessful folder rename
							$Messages->add( sprintf( T_('You cannot choose the new login "%s" (cannot rename user fileroot)'), $this->login), 'error' );
							$update_success = false;
						}
					}
				}
				if( $update_success )
				{
					$Messages->add( T_('Profile has been updated.'), 'success' );
					syslog_insert( sprintf( 'User %s was renamed to %s', '[['.$user_old_login.']]', '[['.$this->login.']]' ), 'info', 'user', $this->ID );
				}
			}

			if( isset( $this->dbchanges_flags['user_tags'] ) )
			{ // Let's handle the tags:
				//die( var_dump( $this->tags ) );
				$this->insert_update_user_tags( 'update' );
			}

			if( $update_success )
			{
				$user_field_url = $this->get_field_url( true );
				if( $user_field_url != '' )
				{ // Update url from extra user fields
					$this->set( 'url', $user_field_url );
					$this->dbsave();
				}

				$DB->commit();
			}
			else
			{
				$DB->rollback();
			}

			if( count( $this->significant_changed_values ) > 0 )
			{ // Send notification email about the changes of user account
				$this->send_account_changed_notification();
			}

			// Send notification to owners of lists where user is subscribed/unsubscribed:
			$this->send_list_owner_notifications();
		}
		elseif( $is_new_user )
		{	// Some error on inserting new user in DB:
			$DB->rollback();
			$update_success = false;
			$Messages->add( 'New user creation error', 'error' );
		}
		else
		{	// Nothing to update:
			$DB->commit();
			$update_success = true;
		}

		// Update user status settings
		if( ( $is_new_user || ( $is_admin_form && $this->ID != 1 ) ) && ( ! $this->update_status_from_Request( true ) ) )
		{
			$Messages->add( T_( 'User status couldn\'t be updated!' ), 'error' );
		}

		$try_to_send_validate_email = ( $is_new_user || $user_was_activated || ( $user_old_email != $this->email ) );
		if( $update_success && $try_to_send_validate_email && $this->check_status( 'can_be_validated' ) )
		{ // user was deactivated somehow ( or it was just created right now ), check if we need to send validation email
			if( ( $Settings->get( 'validation_process' ) != 'easy' ) && ( $current_User->ID != $this->ID ) )
			{ // validation process is set to secure, we may send activation email only if this user is the current_User
				$reg_settings_url = 'href="'.url_add_param( $admin_url, 'ctrl=registration' ).'#fieldset_wrapper_account_activation"';
				$Messages->add( sprintf( T_( 'Because the Account Activation Process is set to <a %s>Secure</a>, the user will have to request a new activation email next time he tries to log in.' ), $reg_settings_url ), 'note' );
			}
			else
			{ // validation process is easy, send email with permanent activation link
				if( $this->send_validate_email( NULL, $blog, !$is_new_user && ( $user_old_email != $this->email ) ) ) // $is_new_user check added to prevent regeneration of $reminder_key when creating new user accounts with welcome campaign email
				{
					if( $current_User->ID == $this->ID )
					{
						$redirect_to = NULL;
						if( ( !is_admin_page() ) && ( !empty( $blog ) ) )
						{ // set where to redirect in front office, another way it would go to profile_update.php
							$BlogCache = & get_BlogCache();
							$Collection = $Blog = $BlogCache->get_by_ID( $blog, false );
							$redirect_to = rawurlencode( $Blog->get( 'userprefsurl' ) );
						}
						$activate_info_link = 'href="'.get_activate_info_url( $redirect_to, '&amp;' ).'"';
						$Messages->add( sprintf( T_('An email has been sent to your email address (%s). Please click on the link therein to activate your account. <a %s>More info &raquo;</a>' ), $this->dget('email'), $activate_info_link ), 'success' );
					}
					else
					{
						$Messages->add( sprintf( T_('An account activation email has been sent to %s' ), $this->login ), 'success' );
					}
				}
				elseif( isset( $demo_mode ) && $demo_mode )
				{
					$Messages->add( 'Could not send activation email. Sending emails is disabled in demo mode.', 'note' );
				}
			}
		}

		// Update user settings:
		$is_preferences_form = param( 'preferences_form', 'boolean', false );
		$is_subscriptions_form = param( 'subscriptions_form', 'boolean', false );
		if( $is_preferences_form || $is_subscriptions_form || $is_admin_form )
		{ // Update UserSettings
			if( $UserSettings->dbupdate() && ( $is_preferences_form || $is_subscriptions_form ) )
			{ // Show feature settings update successful message on user preferences form
				$Messages->add( T_('User feature settings have been changed.'), 'success');
			}
		}

		return true;
	}


	/**
	 * Handle close user account request and update account close reason
	 *
	 * @param boolean save modifications or not. User false only in case when User and UserSettings object will be saved later.
	 * @return boolean true on success, false otherwise
	 */
	function update_status_from_Request( $dbsave, $new_status = NULL )
	{
		global $DB, $Settings, $UserSettings, $Messages, $current_User, $servertimenow;
		global $is_api_request;

		if( ! is_logged_in() || ! $current_User->can_moderate_user( $this->ID ) && ! ( $Settings->get( 'account_close_enabled' ) && $current_User->ID == $this->ID ) )
		{	// Only moderators can update user status:
			$Messages->add_to_group( T_( 'You do not have permission to perform this action' ), 'error', T_('Close account').':' );
			return false;
		}

		if( $dbsave )
		{ // save required
			$DB->begin();
		}

		$edited_user_status = param( 'edited_user_status', 'string' );
		if( ! $is_api_request || ( $is_api_request && isset( $edited_user_status ) ) )
		{
			if( empty( $new_status ) )
			{
				$new_status = param( 'edited_user_status', 'string', true );
			}

			// a close reason type thst is selected from list
			$account_close_type = param( 'account_close_type', 'string', '' );
			// a close reason text - max 255 characters
			$account_close_reason = substr( param( 'account_close_reason', 'text', '' ), 0, 255 );
			if( ( !$this->check_status( 'is_closed' ) ) && ( $new_status == 'closed' ) )
			{ // account was not closed yet
				if( empty( $account_close_reason ) && empty( $account_close_type ) )
				{ // Default value of reason
					$account_close_reason = T_( 'Other' );
				}
				$this->set( 'status', 'closed' );
				$UserSettings->set( 'account_close_ts', $servertimenow, $this->ID );
				$UserSettings->set( 'account_close_reason', trim( $account_close_type.' '.$account_close_reason ), $this->ID );
				// delete last activation email data, this user must not be allowed to reactivate the account ( only admin users may change the status again )
				$UserSettings->delete( 'last_activation_reminder_key', $this->ID );
				$UserSettings->delete( 'last_activation_email', $this->ID );
				// create query to clear all session's of the user
				$clear_sessions_query = 'UPDATE T_sessions
									SET sess_key = NULL
									WHERE sess_user_ID = '.$DB->quote( $this->ID );
				// unsubscribe user from all lists:
				$subscribed_newletter_IDs = $this->get_newsletter_subscriptions( 'subscribed' );
				$this->unsubscribe( $subscribed_newletter_IDs, array( 'user_account_closed' => true ) );

				if( $dbsave && $this->dbupdate() && $UserSettings->dbupdate() && ( $DB->query( $clear_sessions_query ) !== false ) )
				{ // all db modification was successful
					$DB->commit();
					if( $current_User->ID != $this->ID )
					{	// If admin closed some user account
						// Send notification email about closed account to users with edit users permission
						$email_template_params = array(
								'login'           => $this->login,
								'email'           => $this->email,
								'reason'          => $account_close_reason,
								'user_ID'         => $this->ID,
								'closed_by_admin' => $current_User->get_username(),
								'days_count'      => $this->get_days_count_close()
							);
						send_admin_notification( NT_('User account closed'), 'account_closed', $email_template_params );
					}

					// Send notification to owners of list where user was automatically unsubscribed:
					$this->send_list_owner_notifications( 'unsubscribe' );

					return true;
				}
			}
			else
			{
				// set status
				$this->set( 'status', $new_status );
				$UserSettings->set( 'account_close_reason', $account_close_reason, $this->ID );
				if( $dbsave && $this->dbupdate() )
				{ // db update
					$UserSettings->dbupdate();
					$DB->commit();
					return true;
				}
			}

			if( $dbsave )
			{ // save was required, but wasn't successful
				$DB->rollback();
				return false;
			}
		}

		return true;
	}


	/**
	 * Update profileupdate date. Call after a publicly visible user property was updated.
	 */
	function set_profileupdate_date()
	{
		global $current_User, $localtimenow;
		if( ( !empty( $current_User ) ) && ( $this->ID == $current_User->ID ) )
		{
			$this->set( 'profileupdate_date', date( 'Y-m-d H:i:s', $localtimenow ) );
		}
	}


	/**
	 * Update user avatar file
	 *
	 * @param integer the new avatar file ID
	 * @param boolean TRUE to restore this file
	 * @return mixed true on success, allowed action otherwise
	 */
	function update_avatar( $file_ID, $restore = false )
	{
		global $current_User, $Messages;

		$can_moderate_user = $current_User->can_moderate_user( $this->ID );
		if( ! $can_moderate_user && ( $restore || $this->ID != $current_User->ID ) )
		{ // user is only allowed to update him/herself
			$Messages->add( T_('You are not allowed to update this profile!'), 'error' );
			return 'view';
		}

		if( $file_ID == NULL )
		{
			$Messages->add( T_('Profile picture could not be changed!'), 'error' );
			return 'edit';
		}

		$FileCache = & get_FileCache();
		$File = $FileCache->get_by_ID( $file_ID );

		if( ! $File->get( 'can_be_main_profile' ) && ! $can_moderate_user )
		{ // Deny to restore picture if current user has no perm
			$Messages->add( T_('Profile picture could not be changed!'), 'error' );
			return 'edit';
		}

		if( $File->_FileRoot->type == 'user' && $File->_FileRoot->in_type_ID != $this->ID )
		{ // don't allow to use pictures from other users
			$Messages->add( T_('Profile picture could not be changed!'), 'error' );
			return 'edit';
		}

		$restored_success = false;
		if( $restore && $can_moderate_user )
		{ // Restore profile picture
			$File->set( 'can_be_main_profile', '1' );
			$restored_success = $File->dbupdate();
		}

		$this->set( 'avatar_file_ID', $file_ID, true );
		// update profileupdate_date, because a publicly visible user property was changed
		$this->set_profileupdate_date();
		$this->dbupdate();

		if( $restored_success )
		{
			$Messages->add( T_('Profile picture has been restored.'), 'success' );
		}
		else
		{
			$Messages->add( T_('Profile picture has been changed.'), 'success' );
		}

		// Send notification email about the changes of user account
		$this->send_account_changed_notification( true );

		return true;
	}


	/**
	 * Get the rotate avatar icons
	 *
	 * @param integer File ID
	 * @param array Params
	 * @return string HTML text with 3 icons to rotate avatar
	 */
	function get_rotate_avatar_icons( $file_ID, $params = array() )
	{
		// Make sure we are not missing any param:
		$params = array_merge( array(
				'before'   => '<br />',
				'after'    => '',
				'text'     => '',
				'user_tab' => 'avatar',
			), $params );

		// Init links to rotate avatar
		if( is_admin_page() )
		{ // Back-office
			$url_rotate_90_left = regenerate_url( '', 'user_tab='.$params['user_tab'].'&user_ID='.$this->ID.'&action=rotate_avatar_90_left&file_ID='.$file_ID.'&'.url_crumb( 'user' ), '', '&' );
			$url_rotate_180 = regenerate_url( '', 'user_tab='.$params['user_tab'].'&user_ID='.$this->ID.'&action=rotate_avatar_180&file_ID='.$file_ID.'&'.url_crumb( 'user' ), '', '&') ;
			$url_rotate_90_right = regenerate_url( '', 'user_tab='.$params['user_tab'].'&user_ID='.$this->ID.'&action=rotate_avatar_90_right&file_ID='.$file_ID.'&'.url_crumb( 'user' ), '', '&' );
		}
		else
		{ // Front-office
			global $Collection, $Blog;
			$url_rotate_90_left = get_htsrv_url().'profile_update.php?user_tab='.$params['user_tab'].'&blog='.$Blog->ID.'&user_ID='.$this->ID.'&action=rotate_avatar_90_left&file_ID='.$file_ID.'&'.url_crumb( 'user' );
			$url_rotate_180 = get_htsrv_url().'profile_update.php?user_tab='.$params['user_tab'].'&blog='.$Blog->ID.'&user_ID='.$this->ID.'&action=rotate_avatar_180&file_ID='.$file_ID.'&'.url_crumb( 'user' );
			$url_rotate_90_right = get_htsrv_url().'profile_update.php?user_tab='.$params['user_tab'].'&blog='.$Blog->ID.'&user_ID='.$this->ID.'&action=rotate_avatar_90_right&file_ID='.$file_ID.'&'.url_crumb( 'user' );
		}

		$html = $params['before'];

		$html .= action_icon( T_('Rotate this picture 90&deg; to the left'), 'rotate_left', $url_rotate_90_left, '', 0, 0, array( 'style' => 'margin-right:4px' ) );
		$html .= action_icon( T_('Rotate this picture 180&deg;'), 'rotate_180', $url_rotate_180, '', 0, 0, array( 'style' => 'margin-right:4px' ) );
		$html .= action_icon( T_('Rotate this picture 90&deg; to the right'), 'rotate_right', $url_rotate_90_right, $params['text'], empty( $params['text'] ) ? 0: 3, empty( $params['text'] ) ? 0: 4 );

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

		return $html;
	}


	/**
	 * Get file object by ID of this user
	 *
	 * @param integer Avatar file ID
	 * @param string Error code on denied action, It is updated by reference:
	 *        'only_own_profile' - User can update only own profile
	 *        'wrong_file'       - Request with wrong file ID
	 *        'other_user'       - Restricted to edit files from other users
	 * @return object|boolean File object on success, FALSE on failed
	 */
	function & get_File_by_ID( $file_ID, & $error_code )
	{
		global $current_User, $Messages;

		$error_code = false;

		$can_moderate_user = $current_User->can_moderate_user( $this->ID );
		if( ! $can_moderate_user && $this->ID != $current_User->ID )
		{ // user is only allowed to update him/herself
			$Messages->add( T_('You are only allowed to update your own profile!'), 'error' );
			$error_code = 'only_own_profile';
			$r = false;
			return $r;
		}

		if( empty( $file_ID ) )
		{ // File ID is empty
			$Messages->add( T_('You did not specify a file.'), 'error' );
			$error_code = 'wrong_file';
			$r = false;
			return $r;
		}

		$FileCache = & get_FileCache();
		$File = & $FileCache->get_by_ID( $file_ID, false, false );
		if( empty( $File ) )
		{ // File does't exist
			$Messages->add( T_('The requested file does not exist!'), 'error' );
			$error_code = 'wrong_file';
			$r = false;
			return $r;
		}

		if( $File->_FileRoot->type != 'user' || ( $File->_FileRoot->in_type_ID != $this->ID && ! $can_moderate_user ) )
		{ // don't allow use the pictures from other users
			$Messages->add( T_('The requested file doesn\'t belong to this user.'), 'error' );
			$error_code = 'other_user';
			$r = false;
			return $r;
		}

		return $File;
	}


	/**
	 * Rotate user avatar file
	 *
	 * @param integer the new avatar file ID
	 * @param integer Degrees to rotate
	 * @return object|string File object on success;
	 *         Error code on denied action:
	 *          'only_own_profile' - User can update only own profile
	 *          'wrong_file'       - Request with wrong file ID
	 *          'other_user'       - Restricted to edit files from other users
	 *          'rotate_error'     - Some errors in rotate function
	 */
	function rotate_avatar( $file_ID, $degrees )
	{
		$File = & $this->get_File_by_ID( $file_ID, $error_code );
		if( ! $File )
		{ // The file cannot be used for this user, return error code
			return $error_code;
		}

		global $Messages;
		load_funcs( 'files/model/_image.funcs.php' );

		if( !rotate_image( $File, $degrees ) )
		{ // Some errors were during rotate the avatar
			$Messages->add( T_('Profile picture could not be rotated!'), 'error' );
			return 'rotate_error';
		}

		$Messages->add( T_('Profile picture has been rotated.'), 'success' );
		return true;
	}


	/**
	 * Rotate user avatar file
	 *
	 * @param integer the new avatar file ID
	 * @param integer X coordinate (in percents)
	 * @param integer Y coordinate (in percents)
	 * @param integer Width (in percents)
	 * @param integer Height (in percents)
	 * @return object|string File object on success;
	 *         Error code on denied action:
	 *          'only_own_profile' - User can update only own profile
	 *          'wrong_file'       - Request with wrong file ID
	 *          'other_user'       - Restricted to edit files from other users
	 *          'crop_error'       - Some errors in crop function
	 */
	function crop_avatar( $file_ID, $x, $y, $width, $height )
	{
		$File = & $this->get_File_by_ID( $file_ID, $error_code );
		if( ! $File )
		{ // The file cannot be used for this user, return error code
			return $error_code;
		}

		global $Messages, $Settings;
		load_funcs( 'files/model/_image.funcs.php' );

		if( ! crop_image( $File, $x, $y, $width, $height, intval( $Settings->get( 'min_picture_size' ) ), 1024 ) )
		{ // Some errors were during rotate the avatar
			$Messages->add( T_('Profile picture could not be cropped!'), 'error' );
			return 'crop_error';
		}

		$Messages->add( T_('Profile picture has been cropped.'), 'success' );
		return true;
	}


	/**
	 * Get the crop avatar icon
	 *
	 * @param integer File ID
	 * @param array Params
	 * @return string HTML text with icon to crop avatar
	 */
	function get_crop_avatar_icon( $file_ID, $params = array() )
	{
		// Make sure we are not missing any param:
		$params = array_merge( array(
				'before'   => '',
				'after'    => '',
				'text'     => '',
				'user_tab' => 'avatar',
				'onclick'  => '',
			), $params );

		// Init links to rotate avatar
		if( is_admin_page() )
		{ // Back-office
			//$url_crop = regenerate_url( 'user_tab', 'user_tab=crop&user_ID='.$this->ID.'&file_ID='.$file_ID.'&'.url_crumb( 'user' ), '', '&' );
			$url_crop = '#';
		}
		else
		{ // Front-office
			global $Collection, $Blog;
			$url_crop = url_add_param( $Blog->gen_blogurl(), 'disp=avatar&action=crop&file_ID='.$file_ID );
		}

		$html = $params['before'];

		$link_params = array();
		if( ! empty( $params['onclick'] ) )
		{
			$link_params['onclick'] = $params['onclick'];
		}
		$html .= action_icon( T_('Crop this picture'), 'crop', $url_crop, $params['text'], ( empty( $params['text'] ) ? 0: 3 ), ( empty( $params['text'] ) ? 0: 4 ), $link_params );

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

		return $html;
	}


	/**
	 * Remove user avatar
	 *
	 * @return mixed true on success, false otherwise
	 */
	function remove_avatar( $forbid = false )
	{
		global $current_User, $Messages;

		$can_moderate_user = $current_User->can_moderate_user( $this->ID );
		if( ! $can_moderate_user && ( $forbid || $this->ID != $current_User->ID ) )
		{ // user is only allowed to update him/herself
			$Messages->add( T_('You are not allowed to update this profile!'), 'error' );
			return false;
		}

		$forbidden_success = false;
		if( $forbid && $can_moderate_user )
		{ // Forbid profile picture
			$forbidden_file_ID = $this->get( 'avatar_file_ID' );
			$FileCache = & get_FileCache();
			if( ! ( $forbidden_File = & $FileCache->get_by_ID( $forbidden_file_ID, false, false ) ) )
			{ // Broken avatar file
				$Messages->add( T_('Profile picture is not found!'), 'error' );
				return false;
			}

			$forbidden_File->set( 'can_be_main_profile', '0' );
			$forbidden_success = $forbidden_File->dbupdate();
		}

		$this->set( 'avatar_file_ID', NULL, true );
		$this->dbupdate();

		if( $forbidden_success )
		{
			$Messages->add( T_('Profile picture has been forbidden.'), 'success' );
		}
		else
		{
			$Messages->add( T_('Profile picture has been removed.'), 'success' );
		}

		return true;
	}


	/**
	 * Delete user avatar file
	 *
	 * @param integer the avatar file ID
	 * @return mixed true on success, allowed action otherwise
	 */
	function delete_avatar( $file_ID )
	{
		global $current_User, $Messages;

		if( ! $current_User->can_moderate_user( $this->ID ) && $this->ID != $current_User->ID )
		{ // user is only allowed to update him/herself
			$Messages->add( T_('You are only allowed to update your own profile!'), 'error' );
			return 'view';
		}

		if( $file_ID == NULL )
		{
			$Messages->add( T_('Profile picture could not be changed!'), 'error' );
			return 'edit';
		}

		$LinkOwner = new LinkUser( $this );
		$Link = & $LinkOwner->get_link_by_file_ID( $file_ID );
		if( $Link )
		{
			$File = $Link->get_File();
			$LinkOwner->remove_link( $Link );
		}
		else
		{
			$FileCache = & get_FileCache();
			$File = $FileCache->get_by_ID( $file_ID );
		}

		if( $file_ID == $this->avatar_file_ID )
		{	// Unset this picture from user avatar if it is used as main picture
			$this->set( 'avatar_file_ID', NULL, true );
			$this->dbupdate();
		}

		if( ( $File->_FileRoot->type == 'user' ) && ( $File->_FileRoot->in_type_ID == $this->ID )
			&& ( strpos( $File->get_rdfp_rel_path(), 'profile_pictures' ) === 0 ) )
		{ // Delete the file if it is in the current User profile_pictures folder
			$File->unlink();
		}

		$Messages->add( T_('Profile picture has been deleted.'), 'success' );
		return true;
	}


	/**
	 * Update user avatar file to the currently uploaded file
	 *
	 * @return mixed true on success, allowed action otherwise.
	 */
	function update_avatar_from_upload()
	{
		global $current_User, $Messages, $Settings;

		if( ! $current_User->can_moderate_user( $this->ID ) && $this->ID != $current_User->ID )
		{ // user is only allowed to update him/herself
			$Messages->add( T_('You are only allowed to update your own profile!'), 'error' );
			return 'view';
		}

		// process upload
		$FileRootCache = & get_FileRootCache();
		$root = FileRoot::gen_ID( 'user', $this->ID );
		$result = process_upload( $root, 'profile_pictures', true, false, true, false, $Settings->get( 'min_picture_size' ) );
		if( empty( $result ) )
		{
			$Messages->add( T_( 'You don\'t have permission to selected user file root.' ), 'error' );
			return 'view';
		}

		$uploadedFiles = $result['uploadedFiles'];
		if( !empty( $uploadedFiles ) )
		{ // upload was successful
			$File = $uploadedFiles[0];
			$duplicated_files = $File->get_duplicated_files( array( 'root_ID' => $this->ID ) );
			if( ! empty( $duplicated_files ) )
			{ // The file is the duplicate of other profile picture, we should delete it
				$File->dbdelete();
				$Messages->add( T_( 'It seems you are trying to upload the same profile picture twice.' ), 'error' );
				return 'edit';
			}
			elseif( $File->is_image() )
			{ // uploaded file is an image
				$LinkOwner = new LinkUser( $this );
				$File->link_to_Object( $LinkOwner );
				$avatar_changed = false;
				if( empty( $this->avatar_file_ID ) )
				{ // set uploaded image as avatar
					$this->set( 'avatar_file_ID', $File->ID, true );
					// update profileupdate_date, because a publicly visible user property was changed
					$this->set_profileupdate_date();
					$this->dbupdate();
					$avatar_changed = true;
					$Messages->add( T_('Profile picture has been changed.'), 'success' );
				}
				else
				{ // User already has the avatar
					$Messages->add( T_('New picture has been uploaded.'), 'success' );
				}
				// Clear previous Links to load new uploaded file
				$LinkOwner->clear_Links();
				// Send notification email about the changes of user account
				$this->send_account_changed_notification( $avatar_changed, $File->ID );
				return true;
			}
			else
			{ // uploaded file is not an image, delete the file
				$Messages->add( T_( 'The file you uploaded does not seem to be an image.' ) );
				$File->unlink();
			}
		}

		$failedFiles = $result['failedFiles'];
		if( !empty( $failedFiles ) )
		{
			$Messages->add( $failedFiles[0] );
		}

		return 'edit';
	}


	/**
	 * Get session param from the user last session
	 *
	 * @param string param name
	 * @return mixed param value
	 */
	function get_last_session_param( $parname )
	{
		global $DB;

		$parname = 'sess_'.$parname;
		$query = 'SELECT sess_ID, '.$parname.'
					FROM T_sessions
					WHERE sess_user_ID = '.$this->ID.'
					ORDER BY sess_ID DESC
					LIMIT 1';
		$result = $DB->get_row( $query );
		if( !empty( $result ) )
		{
			return format_to_output( $result->$parname );
		}

		return NULL;
	}


	/**
	 * Send a welcome private message
	 */
	function send_welcome_message()
	{
		global $Settings, $UserSettings;

		if( !$Settings->get( 'welcomepm_enabled' ) )
		{	// Sending of welcome PM is disabled
			return false;
		}

		if( $Settings->get( 'welcomepm_notag' ) )
		{	// Don't send welcome PM if user account already has an user tag:
			$user_tags = $this->get_usertags();
			if( ! empty( $user_tags ) )
			{
				return false;
			}
		}

		if( $UserSettings->get( 'welcome_message_sent', $this->ID ) )
		{	// User already received the welcome message
			return false;
		}

		if( !$this->accepts_pm() )
		{	// user can't read private messages, or doesn't want to receive private messages
			return false;
		}

		// Check sender user login for existing
		$UserCache = & get_UserCache();
		$User = $UserCache->get_by_login( $Settings->get( 'welcomepm_from' ) );
		if( !$User )
		{	// Don't send an welcome email if sender login is incorrect
			return false;
		}

		load_class( 'messaging/model/_thread.class.php', 'Thread' );
		load_class( 'messaging/model/_message.class.php', 'Message' );

		// Insert new thread:
		$edited_Thread = new Thread();
		$edited_Message = new Message();
		$edited_Message->Thread = & $edited_Thread;
		$edited_Message->Thread->set( 'title', $Settings->get( 'welcomepm_title' ) );
		$edited_Message->Thread->recipients_list = array( $this->ID );
		$edited_Message->set( 'author_user_ID', $User->ID );
		$edited_Message->creator_user_ID = $User->ID;
		$edited_Message->set( 'text', $Settings->get( 'welcomepm_message' ) );
		if( $edited_Message->dbinsert_individual( $User ) )
		{	// The welcome message was sent/created successfully
			// Change user setting to TRUE in order to don't send it twice
			$UserSettings->set( 'welcome_message_sent', 1, $this->ID );
			$UserSettings->dbupdate();
		}
	}


	/**
	 * Update notification sender's info from General settings
	 *
	 * @param boolean TRUE - to force an updating of the fields
	 */
	function update_sender( $force_update = false )
	{
		global $Settings, $UserSettings;

		if( $force_update || $UserSettings->get( 'notification_sender_email', $this->ID ) == '' )
		{ // Update sender's email
			$UserSettings->set( 'notification_sender_email', $Settings->get( 'notification_sender_email' ), $this->ID );
		}
		if( $force_update || ( ( $UserSettings->get( 'notification_sender_name', $this->ID ) == '' ) && ( $Settings->get( 'notification_sender_name' ) != 'b2evo mailer' ) ) )
		{ // Update sender's name if it was not set and the general settings has different value then the default
			$UserSettings->set( 'notification_sender_name', $Settings->get( 'notification_sender_name' ), $this->ID );
		}

		$UserSettings->dbupdate();
	}


	/**
	 * Get all own blogs of this user which current user can delete
	 *
	 * @return array Blogs
	 */
	function get_deleted_blogs()
	{
		global $DB;

		// Get all own blogs of the edited user
		$BlogCache = & get_BlogCache();
		$BlogCache->ID_array = array();
		$user_Blogs = $BlogCache->load_where( 'blog_owner_user_ID = '.$DB->quote( $this->ID ) );

		$deleted_Blogs = array();
		foreach( $user_Blogs as $user_Blog )
		{
			if( check_user_perm( 'blog_properties', 'edit', false, $user_Blog->ID ) )
			{	// Current user has a permission to delete this blog
				$deleted_Blogs[] = $user_Blog;
			}
		}

		return $deleted_Blogs;
	}


	/**
	 * Delete all blogs of the user recursively
	 *
	 * @return boolean True on success
	 */
	function delete_blogs()
	{
		global $DB, $UserSettings;

		$DB->begin();

		// Get all own blogs of this user which current user can delete
		$deleted_Blogs = $this->get_deleted_blogs();

		foreach( $deleted_Blogs as $deleted_Blog )
		{
			// Delete from DB:
			$deleted_Blog->dbdelete();

			set_working_blog( 0 );
			$UserSettings->delete( 'selected_blog' );	// Needed or subsequent pages may try to access the delete blog
			$UserSettings->dbupdate();
		}

		$DB->commit();

		return true;
	}


	/**
	 * Get the posts of this user which current user can delete
	 *
	 * @param string Type of the deleted posts
	 *               'created'        - the posts created by this user
	 *               'edited'         - the posts edited by this user
	 *               'created|edited' - the posts created OR edited by this user
	 * @return array Items
	 */
	function get_deleted_posts( $type, $check_perm = true )
	{
		global $DB;

		$ItemCache = & get_ItemCache();
		$ItemCache->ID_array = array();
		switch( $type )
		{
			case 'created':
				$user_Items = $ItemCache->load_where( 'post_creator_user_ID = '.$DB->quote( $this->ID ) );
				break;

			case 'edited':
				$user_Items = $ItemCache->load_where( 'post_lastedit_user_ID = '.$DB->quote( $this->ID ) );
				break;

			case 'created|edited':
				$user_Items = $ItemCache->load_where( 'post_lastedit_user_ID = '.$DB->quote( $this->ID ).' OR post_creator_user_ID = '.$DB->quote( $this->ID ) );
				break;
		}

		$deleted_Items = array();
		foreach( $user_Items as $user_Item )
		{
			if( ( ! $check_perm ) || check_user_perm( 'item_post!CURSTATUS', 'delete', false, $user_Item ) )
			{	// Current user has a permission to delete this item
				$deleted_Items[] = $user_Item;
			}
		}

		return $deleted_Items;
	}


	/**
	 * Get the posts of this user which current user can delete
	 *
	 * @param string Type of the deleted posts
	 *               'created'        - the posts created by this user
	 *               'edited'         - the posts edited by this user
	 *               'created|edited' - the posts created OR edited by this user
	 * @param boolean Count only the number of posts that the current user can delete
	 * @return mixed array of Items if $count_only is FALSE otherwise integer
	 */
	function get_deleted_posts2( $type, $count_only = false )
	{
		global $DB, $current_User;

		$current_User_Group = $current_User->get_Group();

		switch( $type )
		{
			case 'created':
				$where_clause = 'post_creator_user_ID = '.$DB->quote( $this->ID );
				break;

			case 'edited':
				$from_add = 'LEFT JOIN ( SELECT iver_itm_ID, COUNT(*) AS counter
						FROM T_items__version
						WHERE iver_edit_user_ID = '.$DB->quote( $this->ID ).'
						GROUP BY iver_itm_ID ) AS b
							ON b.iver_itm_ID = post_ID ';
				$where_clause = 'post_creator_user_ID != '.$DB->quote( $this->ID ).' AND ( b.counter > 0 OR post_lastedit_user_ID = '.$DB->quote( $this->ID ).' )';
				break;

			case 'created|edited':
				$where_clause = '( post_lastedit_user_ID = '.$DB->quote( $this->ID ).' OR post_creator_user_ID = '.$DB->quote( $this->ID ).' )';
				break;
		}

		if( $count_only )
		{
			$sql = 'SELECT COUNT( DISTINCT( post_ID ) ) ';
		}
		else
		{
			$sql = 'SELECT DISTINCT T_items__item.* ';
		}

		$sql .= 'FROM T_items__item ';
		if( ! empty( $from_add ) )
		{
			$sql .= $from_add;
		}
		$sql .= 'LEFT JOIN (
					SELECT postcat_post_ID,
						COUNT( * ) AS categories,
						SUM( IF( cat_lock = 1, 1, 0 ) ) AS locked_categories
					FROM T_postcats
					LEFT JOIN T_categories
						ON cat_ID = postcat_cat_ID
					GROUP BY postcat_post_ID
				) AS a
					ON a.postcat_post_ID = post_ID
				LEFT JOIN T_postcats AS b
					ON b.postcat_post_ID = post_ID
				LEFT JOIN T_categories
					ON cat_ID = b.postcat_cat_ID
				LEFT JOIN T_blogs
					ON blog_ID = cat_blog_ID
				LEFT JOIN T_coll_user_perms
					ON bloguser_blog_ID = blog_ID AND bloguser_user_ID = '.$DB->quote( $current_User->ID ).'
				LEFT JOIN T_coll_group_perms
					ON bloggroup_blog_ID = blog_ID AND bloggroup_group_ID = '.$DB->quote( $current_User_Group->ID ).'
				LEFT JOIN T_groups__groupsettings
					ON gset_grp_ID = bloggroup_group_ID AND gset_name = "perm_users"
				LEFT JOIN (
					SELECT
						bloggroup_blog_ID,
						SUM( IF( bloggroup_perm_delpost = 1, 1, 0 ) ) AS secondary_grp_perm_delpost,
						SUM( IF( bloggroup_perm_cats = 1, 1, 0 ) ) AS secondary_grp_perm_cats
					FROM T_users__secondary_user_groups
					LEFT JOIN T_groups
						ON sug_grp_ID = grp_ID
					LEFT JOIN T_coll_group_perms
						ON bloggroup_group_ID = grp_ID
					WHERE
						sug_user_ID = '.$DB->quote( $current_User->ID ).'
					GROUP BY
						bloggroup_blog_ID
				) AS sg
					ON sg.bloggroup_blog_ID = blog_ID
				WHERE
					'.$where_clause.'
					AND
					(
				 	  ( categories > locked_categories OR ( bloguser_perm_cats = 1 OR bloggroup_perm_cats = 1 OR secondary_grp_perm_cats > 0 ) )
						AND
						(
							( blog_advanced_perms = 0 && blog_owner_user_ID = '.$DB->quote( $current_User->ID ).' )
							OR
							( blog_advanced_perms = 1 && ( bloguser_perm_delpost = 1 OR bloggroup_perm_delpost = 1 OR secondary_grp_perm_delpost > 0 ) )
						)
						OR
						( gset_value = "edit" )
					)';

		if( $count_only )
		{
			return $DB->get_var( $sql );
		}
		else
		{
			$user_Items = $DB->get_results( $sql );

			$deleted_Items = array();
			foreach( $user_Items as $r => $row )
			{
				$deleted_Items[] = new Item( $row );
			}

			return $deleted_Items;
		}
	}


	/**
	 * Delete posts of the user
	 *
	 * @param string Type of the deleted posts
	 *               'created'        - the posts created by this user
	 *               'edited'         - the posts edited by this user
	 *               'created|edited' - the posts created OR edited by this user
	 * @return boolean True on success
	 */
	function delete_posts( $type )
	{
		global $DB, $Plugins, $current_User;

		$DB->begin();

		// Get the posts of this user which current user can delete.
		// Note: If current user can moderate this user then it is allowed to delete all user data even if it wouldn't be allowed otherwise.
		$check_perm = ( ( $type != 'created' ) || ( ! $current_User->can_moderate_user( $this->ID ) ) );
		$deleted_Items = $this->get_deleted_posts( $type, $check_perm );

		foreach( $deleted_Items as $deleted_Item )
		{
			$Plugins->trigger_event( 'AdminBeforeItemEditDelete', array( 'Item' => & $deleted_Item ) );

			// Delete from DB:
			$deleted_Item->dbdelete();
		}

		$DB->commit();

		return true;
	}


	/**
	 * Get the comments IDs that were created by this user
	 *
	 * @return array IDs of the own comments
	 */
	function get_own_comments_IDs()
	{
		global $DB;

		// Get the comments of the user
		$SQL = new SQL( 'Get the comments of user #'.$this->ID );
		$SQL->SELECT( 'comment_ID' );
		$SQL->FROM( 'T_comments' );
		$SQL->WHERE( 'comment_author_user_ID = '.$DB->quote( $this->ID ) );

		return $DB->get_col( $SQL );
	}


	/**
	 * Check if current has at least one own comment to delete
	 *
	 * @return array Comments IDs
	 */
	function has_comment_to_delete()
	{
		global $current_User;

		$comments_IDs = $this->get_own_comments_IDs();

		if( ! count( $comments_IDs ) )
		{ // User has no comments
			return false;
		}

		if( $current_User->can_moderate_user( $this->ID ) )
		{ // If current user can moderate this user then it is allowed to delete all user data even if it wouldn't be allowed otherwise.
			return true;
		}

		$CommentCache = & get_CommentCache();

		foreach( $comments_IDs as $comment_ID )
		{
			if( ( $user_Comment = & $CommentCache->get_by_ID( $comment_ID, false, false ) ) &&
				  check_user_perm( 'comment!CURSTATUS', 'delete', false, $user_Comment ) )
			{ // Current user has a permission to delete this comment
				return true;
			}
			// Clear a cache to avoid a memory allocation error
			$CommentCache->clear();
		}

		return false;
	}


	/**
	 * Delete comments of the user
	 *
	 * @return boolean True on success
	 */
	function delete_comments()
	{
		global $DB, $current_User;

		// If user has a huge amount of the comments it will takes many time to delete all comments
		set_max_execution_time( 900 );

		$DB->begin();

		// Get the comments of this user which current user can delete
		$comments_IDs = $this->get_own_comments_IDs();

		if( ! count( $comments_IDs ) )
		{ // User has no comments
			return false;
		}

		$CommentCache = & get_CommentCache();
		$ItemCache = & get_ItemCache();

		// If current user can moderate this user then it is allowed to delete all user data even if it wouldn't be allowed otherwise.
		$current_user_can_moderate = $current_User->can_moderate_user( $this->ID );

		$result = false;
		foreach( $comments_IDs as $comment_ID )
		{
			$deleted_Comment = & $CommentCache->get_by_ID( $comment_ID, false, false );
			if( $deleted_Comment &&
			    ( $current_user_can_moderate ||
			      check_user_perm( 'comment!CURSTATUS', 'delete', false, $deleted_Comment ) ) )
			{ // Current user has a permission to delete this comment
				// Delete from DB
				$result = $deleted_Comment->dbdelete( true, false );
				if( ! $result )
				{
					break;
				}
			}
			// Clear a cache to avoid a memory allocation error
			$CommentCache->clear();
			$ItemCache->clear();
		}

		if( $result )
		{
			$DB->commit();
		}
		else
		{
			$DB->rollback();
		}

		return $result;
	}


	/**
	 * Delete private messaged of the user
	 *
	 * @param string Type ( sent | received )
	 * @return boolean True on success
	 */
	function delete_messages( $type = 'sent' )
	{
		global $DB, $Plugins, $current_User;

		// Check permissions
		// Note: If current user can moderate this user then it is allowed to delete all user data even if it wouldn't be allowed otherwise
		if( ! $current_User->can_moderate_user( $this->ID ) )
		{ // Note: if users have delete messaging perms then they can delete any user messages ( Of course only if the delete action is available/displayed for them )
			check_user_perm( 'perm_messaging', 'delete', true );
		}

		$DB->begin();

		$MessageCache = & get_MessageCache();
		$MessageCache->clear();
		if( $type == 'received' )
		{	// Received messages:
			$SQL = $MessageCache->get_SQL_object();
			$SQL->FROM_add( 'INNER JOIN T_messaging__threadstatus ON tsta_thread_ID = msg_thread_ID' );
			$SQL->WHERE( 'tsta_user_ID = '.$DB->quote( $this->ID ) );
			$SQL->WHERE_and( 'msg_author_user_ID != '.$DB->quote( $this->ID ) );
			$MessageCache->load_by_sql( $SQL );
		}
		else
		{	// Sent messages:
			$MessageCache->load_where( 'msg_author_user_ID = '.$this->ID );
		}

		$thread_IDs = array();
		while( ( $iterator_Message = & $MessageCache->get_next() ) != NULL )
		{ // Iterate through MessageCache
			// Delete a message from DB:
			$msg_thread_ID = $iterator_Message->get( 'thread_ID' );
			if( $iterator_Message->dbdelete() )
			{
				$thread_IDs[ $msg_thread_ID ] = NULL;
			}
		}

		if( ! empty( $thread_IDs ) )
		{	// Delete read statuses of the threads where message was deleted:
			$DB->query( 'DELETE FROM T_messaging__threadstatus
				WHERE tsta_user_ID = '.$DB->quote( $this->ID ).'
				  AND tsta_thread_ID IN ( '.$DB->quote( array_keys( $thread_IDs ) ).' )' );
		}

		$DB->commit();

		return true;
	}


	/**
	 * Delete polls of the user
	 *
	 * @return boolean True on success
	 */
	function delete_polls()
	{
		global $DB, $current_User;

		// If current user can moderate this user then it is allowed to delete all user data even if it wouldn't be allowed otherwise.
		$current_user_can_moderate = $current_User->can_moderate_user( $this->ID );

		$DB->begin();

		$PollCache = & get_PollCache();
		$PollCache->clear();
		$PollCache->load_where( 'pqst_owner_user_ID = '.$this->ID );

		$result = false;
		while( ( $iterator_Poll = & $PollCache->get_next() ) != NULL )
		{	// Iterate through PollCache:
			if( $current_user_can_moderate ||
			    check_user_perm( 'polls', 'edit', false, $iterator_Poll ) )
			{ // Current user has a permission to delete this poll
				// Delete the poll from DB:
				$result = $iterator_Poll->dbdelete();
				if( ! $result )
				{
					break;
				}
			}
		}

		if( $result )
		{
			$DB->commit();
		}
		else
		{
			$DB->rollback();
		}

		return $result;
	}


	/**
	 * Get number of posts and percent of published posts by this user
	 *
	 * @param array Params
	 * @return string Result
	 */
	function get_reputation_posts( $params = array() )
	{
		// Make sure we are not missing any param:
		$params = array_merge( array(
				'text' => T_( '%s (%s%% are public)' ),
			), $params );

		$total_num_posts = $this->get_num_posts();
		$public_num_posts = $this->get_num_posts( 'published' );
		if( $total_num_posts > 0 )
		{ // Calc percent of published posts
			$public_percent = floor( $public_num_posts / $total_num_posts * 100 );
		}
		else
		{ // To avoid devision by zero
			$public_percent = 0;
		}

		if( $total_num_posts > 0 )
		{	// Make a link to page with user's posts:
			global $current_User;
			if( is_admin_page() && is_logged_in() &&
			    ( $this->ID == $current_User->ID || check_user_perm( 'users', 'view' ) ) )
			{	// For back-office
				global $admin_url;
				$total_num_posts_url = $admin_url.'?ctrl=user&amp;user_tab=activity&amp;user_ID='.$this->ID;
			}
			else
			{	// For front-office
				global $Collection, $Blog;
				if( ! empty( $Blog ) )
				{	// Only if blog is defined
					$total_num_posts_url = url_add_param( $Blog->gen_blogurl(), 'disp=useritems&amp;user_ID='.$this->ID );
				}
			}
		}

		if( empty( $total_num_posts_url ) )
		{	// No link to view posts
			$total_num_posts = '<b>'.$total_num_posts.'</b>';
		}
		else
		{	// Set a posts number as link if it is allowed:
			$total_num_posts = '<a href="'.$total_num_posts_url.'"><b>'.$total_num_posts.'</b></a>';
		}

		return sprintf( $params['text'], $total_num_posts, $public_percent );
	}


	/**
	 * Get number of comments and percent of published comments and number of helpful votes on comments by this user
	 *
	 * @param array Params
	 * @return string Result
	 */
	function get_reputation_comments( $params = array() )
	{
		// Make sure we are not missing any param:
		$params = array_merge( array(
				'view_type'     => 'simple', // 'simple', 'extended'
				'text_simple'   => T_( '%s has posted %s comments (%s%% of which are public). %s of these comments have been found useful by %s different users.' ),
				'text_extended' => T_( '%s has posted %s comments (%s%% of which are public).<br />%s voted useful by %s different users.<br />%s voted NOT useful by %s different users.<br />%s considered OK by %s different users.<br />%s considered SPAM by %s different users.' ),
			), $params );

		$total_num_comments = $this->get_num_comments();
		$public_num_comments = $this->get_num_comments( 'published' );
		if( $total_num_comments > 0 )
		{ // Calc percent of published comments
			$public_percent = floor( $public_num_comments / $total_num_comments * 100 );
		}
		else
		{ // To avoid devision by zero
			$public_percent = 0;
		}

		if( $total_num_comments > 0 )
		{ // Make a link to page with user's comments
			$total_num_comments = '<a href="'.get_dispctrl_url( 'usercomments', 'user_ID='.$this->ID ).'"><b>'.$total_num_comments.'</b></a>';
		}
		else
		{
			$total_num_comments = '<b>'.$total_num_comments.'</b>';
		}

		// Get number of helpful votes on comments for this user
		global $DB;
		$comments_SQL = new SQL();
		if( $params['view_type'] == 'simple' )
		{ // Simple view
			$comments_SQL->SELECT( 'cmvt_user_ID AS user_ID, COUNT(*) AS cnt' );
		}
		else
		{ // Extended view
			$comments_SQL->SELECT( 'cmvt_user_ID AS user_ID, cmvt_helpful, cmvt_spam' );
		}
		$comments_SQL->FROM( 'T_comments' );
		$comments_SQL->FROM_add( 'INNER JOIN T_comments__votes ON comment_ID = cmvt_cmt_ID' );
		$comments_SQL->WHERE( 'comment_author_user_ID = '.$this->ID );
		$comments_SQL->WHERE_and( 'comment_status IN ( "published", "community", "protected", "review" )' );
		if( $params['view_type'] == 'simple' )
		{ // Simple view
			$comments_SQL->WHERE_and( 'cmvt_helpful = 1' );
			$comments_SQL->GROUP_BY( 'user_ID' );
		}

		if( $params['view_type'] == 'simple' )
		{ // Simple view
			$votes = $DB->get_assoc( $comments_SQL->get() );

			// Calculate total votes from all users
			$users_count_useful = count( $votes );
			$votes_count_useful = 0;
			foreach( $votes as $user_votes )
			{
				$votes_count_useful += $user_votes;
			}

			return sprintf( $params['text_simple'],
				$this->get_username(), $total_num_comments, $public_percent,
				'<b class="green">'.$votes_count_useful.'</b>', '<b>'.$users_count_useful.'</b>' );
		}
		else
		{ // Extended view
			$votes = $DB->get_results( $comments_SQL->get() );

			$votes_count_useful = 0;
			$users_count_useful = array();
			$votes_count_not = 0;
			$users_count_not = array();
			$votes_count_ok = 0;
			$users_count_ok = array();
			$votes_count_spam = 0;
			$users_count_spam = array();
			foreach( $votes as $vote )
			{
				if( $vote->cmvt_helpful === '1' )
				{ // Useful votes
					$votes_count_useful++;
					$users_count_useful[ $vote->user_ID ] = NULL;
				}
				elseif( $vote->cmvt_helpful === '-1' )
				{ // Not useful votes
					$votes_count_not++;
					$users_count_not[ $vote->user_ID ] = NULL;
				}
				if( $vote->cmvt_spam === '-1' )
				{ // Ok votes
					$votes_count_ok++;
					$users_count_ok[ $vote->user_ID ] = NULL;
				}
				elseif( $vote->cmvt_spam === '1' )
				{ // Spam votes
					$votes_count_spam++;
					$users_count_spam[ $vote->user_ID ] = NULL;
				}
			}

			return sprintf( $params['text_extended'],
				$this->get_username(), $total_num_comments, $public_percent,
				'<b class="green">'.$votes_count_useful.'</b>', '<b>'.count( $users_count_useful ).'</b>',
				'<b class="red">'.$votes_count_not.'</b>', '<b>'.count( $users_count_not ).'</b>',
				'<b class="green">'.$votes_count_ok.'</b>', '<b>'.count( $users_count_ok ).'</b>',
				'<b class="red">'.$votes_count_spam.'</b>', '<b>'.count( $users_count_spam ).'</b>' );
		}
	}


	/**
	 * Get number of files (and number of helpful votes on photos only) by this user
	 *
	 * @param array Params
	 * @return string Result
	 */
	function get_reputation_files( $params = array() )
	{
		// Make sure we are not missing any param:
		$params = array_merge( array(
				'view_type'           => 'simple', // 'simple', 'extended'
				'file_type'           => 'image', // 'image', 'audio', 'other'
				'text_image_simple'   => T_( '%s has uploaded %s photos. %s of these photos have been liked by %s different users.' ),
				'text_image_extended' => T_( '%s has uploaded %s photos.<br />%s voted up (liked) by %s different users.<br />%s voted down by %s different users.<br />%s considered INAPPROPRIATE by %s different users.<br />%s considered SPAM by %s different users.' ),
				'text_audio'          => T_( '%s has uploaded %s audio files.' ),
				'text_video'          => T_( '%s has uploaded %s video files.' ),
				'text_other'          => T_( '%s has uploaded %s other files.' ),
			), $params );

		switch( $params['file_type'] )
		{
			case 'image':
				// Number of photos
				global $DB;
				// Get number of helpful votes on links for this user
				$links_SQL = new SQL();
				if( $params['view_type'] == 'simple' )
				{ // Simple view
					$links_SQL->SELECT( 'lvot_user_ID AS user_ID, COUNT(*) AS cnt' );
				}
				else
				{ // Extended view
					$links_SQL->SELECT( 'lvot_user_ID AS user_ID, lvot_like, lvot_inappropriate, lvot_spam' );
				}
				$links_SQL->FROM( 'T_links' );
				$links_SQL->FROM_add( 'INNER JOIN T_files ON file_ID = link_file_ID' );
				$links_SQL->FROM_add( 'INNER JOIN T_links__vote ON link_ID = lvot_link_ID' );
				$links_SQL->WHERE( 'link_creator_user_ID = '.$this->ID );
				$links_SQL->WHERE_and( 'file_type = '.$DB->quote( $params['file_type'] ) );
				if( $params['view_type'] == 'simple' )
				{ // Simple view
					$links_SQL->WHERE_and( 'lvot_like = 1' );
					$links_SQL->GROUP_BY( 'user_ID' );
				}


				if( $params['view_type'] == 'simple' )
				{ // Simple view
					$votes = $DB->get_assoc( $links_SQL->get() );

					// Calculate total votes from all users
					$users_count_up = count( $votes );
					$votes_count_up = 0;
					foreach( $votes as $user_votes )
					{
						$votes_count_up += $user_votes;
					}
					return sprintf( $params['text_image_simple'],
						$this->get_username(), '<b>'.$this->get_num_files( 'image' ).'</b>',
						'<b class="green">'.$votes_count_up.'</b>', '<b>'.$users_count_up.'</b>' );
				}
				else
				{ // Extended view
					$votes = $DB->get_results( $links_SQL->get() );

					$votes_count_up = 0;
					$users_count_up = array();
					$votes_count_down = 0;
					$users_count_down = array();
					$votes_count_inappropriate = 0;
					$users_count_inappropriate = array();
					$votes_count_spam = 0;
					$users_count_spam = array();
					foreach( $votes as $vote )
					{
						if( $vote->lvot_like === '1' )
						{ // Up votes
							$votes_count_up++;
							$users_count_up[ $vote->user_ID ] = NULL;
						}
						elseif( $vote->lvot_like === '-1' )
						{ // Down votes
							$votes_count_down++;
							$users_count_down[ $vote->user_ID ] = NULL;
						}
						if( $vote->lvot_inappropriate === '1' )
						{ // Inappropriate votes
							$votes_count_inappropriate++;
							$users_count_inappropriate[ $vote->user_ID ] = NULL;
						}
						if( $vote->lvot_spam === '1' )
						{ // Spam votes
							$votes_count_spam++;
							$users_count_spam[ $vote->user_ID ] = NULL;
						}
					}

					return sprintf( $params['text_image_extended'],
						$this->get_username(), '<b>'.$this->get_num_files( 'image' ).'</b>',
						'<b class="green">'.$votes_count_up.'</b>', '<b>'.count( $users_count_up ).'</b>',
						'<b class="red">'.$votes_count_down.'</b>', '<b>'.count( $users_count_down ).'</b>',
						'<b class="red">'.$votes_count_inappropriate.'</b>', '<b>'.count( $users_count_inappropriate ).'</b>',
						'<b class="red">'.$votes_count_spam.'</b>', '<b>'.count( $users_count_spam ).'</b>' );
				}
				break;

			case 'audio':
				// Number of audio files
				return sprintf( $params['text_audio'], $this->get_username(), '<b>'.$this->get_num_files( 'audio' ).'</b>' );

			case 'video':
				// Number of video files
				return sprintf( $params['text_video'], $this->get_username(), '<b>'.$this->get_num_files( 'video' ).'</b>' );

			case 'other':
				// Number of other files
				return sprintf( $params['text_other'], $this->get_username(), '<b>'.$this->get_num_files( 'other' ).'</b>' );
		}
	}


	/**
	 * Get total size of uploaded files
	 *
	 * @param array Params
	 * @return string Result
	 */
	function get_reputation_total_upload( $params = array() )
	{
		$params = array_merge( array(
				'text' => T_('%s has uploaded a total of %s')
			), $params );

		$total_upload = $this->get_total_upload();
		if( empty( $total_upload ) )
		{
			$total_upload = '0 '.T_('B.');
		}
		else
		{
			$total_upload = bytesreadable( $total_upload );
		}

		return sprintf( $params['text'], $this->get_username(), '<b>'.$total_upload.'</b>' );
	}


	/**
	 * Get number of spam votes which were made by this user
	 *
	 * @param array Params
	 * @return string Result
	 */
	function get_reputation_spam( $params = array() )
	{
		// Make sure we are not missing any param:
		$params = array_merge( array(
				'text' => T_( '%s successfully reported %s spams and/or spammers!' ),
			), $params );

		global $DB, $UserSettings;

		$comments_SQL = new SQL( 'Get number of spam votes on comments by this user' );
		$comments_SQL->SELECT( 'COUNT(*) AS cnt' );
		$comments_SQL->FROM( 'T_comments__votes' );
		$comments_SQL->WHERE( 'cmvt_user_ID = '.$this->ID );
		$comments_SQL->WHERE_and( 'cmvt_spam = 1' );

		$links_SQL = new SQL( 'Get number of spam votes on links by this user' );
		$links_SQL->SELECT( 'COUNT(*) AS cnt' );
		$links_SQL->FROM( 'T_links__vote' );
		$links_SQL->WHERE( 'lvot_user_ID = '.$this->ID );
		$links_SQL->WHERE_and( 'lvot_spam = 1' );

		$votes = $DB->get_var( 'SELECT SUM( cnt )
			FROM ('.$comments_SQL->get().' UNION ALL '.$links_SQL->get().') AS tbl' );

		// Get spam fighter score for the users that were reported and deleted
		$votes += intval( $UserSettings->get( 'spam_fighter_score', $this->ID ) );

		return sprintf( $params['text'], $this->get_username(), '<b>'.$votes.'</b>' );
	}


	/**
	 * Get status of user email
	 *
	 * @return string Status: 'unknown', 'working', 'unattended', 'redemption', 'warning', 'suspicious1', 'suspicious2', 'suspicious3', 'prmerror', 'spammer'
	 */
	function get_email_status()
	{
		$user_email = $this->get( 'email' );

		$EmailAddressCache = & get_EmailAddressCache();
		if( !empty( $user_email ) && $EmailAddress = & $EmailAddressCache->get_by_name( $user_email, false, false ) )
		{ // The email of this user is located in the DB table
			return $EmailAddress->get( 'status' );
		}
		else
		{ // There is no email address in the DB table
			return 'unknown';
		}
	}


	/**
	 * Get domain of user email
	 *
	 * @return string Domain
	 */
	function get_email_domain()
	{
		// Extract domain from email address:
		return preg_replace( '#^[^@]+@#', '', $this->get( 'email' ) );
	}


	/**
	 * Check if user has a permission to moderate the user
	 *
	 * @param integer User ID
	 * @return boolean TRUE on success
	 */
	function can_moderate_user( $user_ID, $assert = false )
	{
		if( $this->check_perm( 'users', 'edit' ) )
		{ // User can edit all users
			return true;
		}

		if( $this->check_perm( 'users', 'moderate', $assert ) )
		{ // User can moderate other user but we should to compare levels of users groups
			$UserCache = & get_UserCache();
			if( $target_User = $UserCache->get_by_ID( $user_ID, false, false ) )
			{
				if( $target_User->get_Group()->get( 'level' ) < $this->get_Group()->get( 'level' ) )
				{ // User can moderate only users with level lower than own level
					return true;
				}
			}
		}

		if( $assert )
		{ // We can't let this go on!
			debug_die( sprintf( T_('User #%s has no permission to edit user #%s!'), $this->ID, $user_ID ) );
		}

		return false;
	}


	/**
	 * Send an email notification when user account has been changed
	 *
	 * @param boolean true if the main profile picture was changed, false otherwise
	 * @param mixed false if there was no new profile picture upload, the newly uploaded file ID otherwise
	 */
	function send_account_changed_notification( $avatar_changed = false, $new_avatar_upload = false )
	{
		if( ! is_logged_in() )
		{ // User must be logged in for this action
			return;
		}

		if( empty( $this->significant_changed_values ) && ( ! $avatar_changed ) && ( ! $new_avatar_upload ) )
		{ // Nothing important was changed, so no need to send changed notification
			return;
		}

		$email_template_params = array(
				'user_ID' => $this->ID,
				'login'   => $this->login,
				'fields'  => array(
					'login'     => array( 'title' => /* TRANS: noun */ NT_('Login') ),
					'grp_ID'  => array( 'title' => NT_('Group'), 'className' => 'Group' ),
					'nickname'  => array( 'title' => NT_('Nickname') ),
					'firstname' => array( 'title' => NT_('First name') ),
					'lastname'  => array( 'title' => NT_('Last name') ),
					'gender'    => array( 'title' => NT_('Gender') ),
					'ctry_ID'   => array( 'title' => NT_('Country'), 'className' => 'Country' ),
					'rgn_ID'    => array( 'title' => NT_('Region'), 'className' => 'Region' ),
					'subrg_ID'  => array( 'title' => NT_('Sub-region'), 'className' => 'Subregion' ),
					'city_ID'   => array( 'title' => NT_('City'), 'className' => 'City' ),
				),
				'avatar_changed'    => $avatar_changed,
				'new_avatar_upload' => $new_avatar_upload,
			);

		foreach( $email_template_params['fields'] as $user_field_name => $user_field_data )
		{ // Set new and previous values for email template
			$field_was_changed = ( isset( $this->significant_changed_values[ $user_field_name ] ) && ( ! empty( $this->significant_changed_values[ $user_field_name ] ) ) );
			if( isset( $user_field_data['className'] ) )
			{ // The field value is an object ID, get the object name
				$Cache = call_user_func( 'get_'.$user_field_data['className'].'Cache' );
				$Object = & $Cache->get_by_ID( $this->get( $user_field_name ), false, false );
				$user_field_data['new'] = empty( $Object ) ? NULL : $Object->get_name();
				if( $field_was_changed )
				{ // Get the old display value of the field
					$Object = & $Cache->get_by_ID( $this->significant_changed_values[ $user_field_name ] );
					$user_field_data['old'] = $Object->get_name();
				}
			}
			elseif( $user_field_name == 'gender' )
			{ // This is the gender field, get the display name of the gender
				$user_field_data['new'] = $this->get_gender();
				if( $field_was_changed )
				{
					$user_field_data['old'] = ( $this->significant_changed_values[ $user_field_name ] == 'M' ) ? T_('A man') : T_('A woman');
				}
			}
			else
			{ // Get the field value
				$user_field_data['new'] = $this->get( $user_field_name );
				if( $field_was_changed )
				{ // The field was changed
					$user_field_data['old'] = $this->significant_changed_values[ $user_field_name ];
				}
			}

			if( !isset( $user_field_data['old'] ) )
			{ // The field was not changed or the old value was empty
				$user_field_data['old'] = array_key_exists( $user_field_name, $this->significant_changed_values ) ? NULL : $user_field_data['new'];
			}

			$email_template_params['fields'][ $user_field_name ] = $user_field_data;
		}

		send_admin_notification( NT_('User profile changed'), 'account_changed', $email_template_params );
		// Clear changed values
		$this->significant_changed_values = array();
	}


	/**
	 * Get organizations that user has selected
	 *
	 * @return array Organizations( key => org_ID, value => array( 'name', 'accepted', 'role' )
	 */
	function get_organizations_data()
	{
		if( ! isset( $this->organizations ) )
		{	// Get the organizations from DB
			global $DB;
			$SQL = new SQL();
			$SQL->SELECT( 'org_ID, org_name, uorg_accepted, uorg_role, uorg_priority' );
			$SQL->FROM( 'T_users__user_org' );
			$SQL->FROM_add( 'INNER JOIN T_users__organization ON org_ID = uorg_org_ID' );
			$SQL->WHERE( 'uorg_user_ID = '.$DB->quote( $this->ID ) );
			$SQL->ORDER_BY( 'uorg_accepted DESC, uorg_org_ID' );
			$organizations = $DB->get_results( $SQL->get() );
			$this->organizations = array();
			foreach( $organizations as $organization )
			{
				$this->organizations[ $organization->org_ID ] = array(
						'name'     => $organization->org_name,
						'accepted' => $organization->uorg_accepted,
						'role'     => $organization->uorg_role,
						'priority' => $organization->uorg_priority,
					);
			}
		}

		return $this->organizations;
	}


	/**
	 * Get organizations that are accepted for this user
	 *
	 * @return array Organizations: array( 'name', 'url' )
	 */
	function get_organizations()
	{
		global $DB;

		$SQL = new SQL();
		$SQL->SELECT( 'org_name AS name, org_url AS url' );
		$SQL->FROM( 'T_users__user_org' );
		$SQL->FROM_add( 'INNER JOIN T_users__organization ON org_ID = uorg_org_ID' );
		$SQL->WHERE( 'uorg_user_ID = '.$DB->quote( $this->ID ) );
		// Organization must be accepted by Admin
		$SQL->WHERE_and( 'uorg_accepted = 1' );
		$SQL->ORDER_BY( 'org_name' );

		return $DB->get_results( $SQL->get() );
	}


	/**
	 * Update user's organizations in DB
	 *
	 * @param array Organization IDs
	 * @param array Organization roles
	 * @param array Organization priorities
	 * @param boolean TRUE to auto accept user to organization (Used on install demo users)
	 */
	function update_organizations( $organization_IDs, $organization_roles = array(), $organization_priorities = array(), $force_accept = false )
	{
		global $DB, $current_User, $Messages;

		$OrganizationCache = & get_OrganizationCache();

		$curr_orgs = $this->get_organizations_data();
		$curr_org_IDs = array_keys( $curr_orgs );
		$insert_orgs = array();
		foreach( $organization_IDs as $o => $organization_ID )
		{
			if( ! ( $user_Organization = & $OrganizationCache->get_by_ID( $organization_ID, false, false ) ) )
			{ // Organization is not selected or doesn't exist in DB, Skip it
				continue;
			}

			// Check permission if current user can edit the organization:
			$perm_edit_orgs = check_user_perm( 'orgs', 'edit', false, $user_Organization );
			if( ! $perm_edit_orgs && $user_Organization->get( 'accept' ) == 'no' )
			{	// Skip this if current user cannot edit the organization and it has a setting to deny a member joining:
				continue;
			}

			if( isset( $insert_orgs[ $organization_ID ] ) )
			{	// Don't join this user to same organization twice:
				$Messages->add( sprintf( T_('You are already a member of "%s".'), $user_Organization->get_name() ), 'error' );
			}
			elseif( in_array( $organization_ID, $curr_org_IDs ) )
			{ // User is already in this organization
				if( $user_Organization->perm_role == 'owner and member' ||
				    ( is_logged_in() && $user_Organization->owner_user_ID == $current_User->ID ) ||
				    ! $curr_orgs[ $organization_ID ]['accepted'] )
				{	// Update role if current user has permission or it is not accepted yet by admin
					$insert_orgs[ $organization_ID ]['role'] = ( empty( $organization_roles[ $o ] ) ? NULL : $organization_roles[ $o ] );
				}
				else
				{	// Don't updated role by current user:
					$insert_orgs[ $organization_ID ]['role'] = $curr_orgs[ $organization_ID ]['role'];
				}

				if( $perm_edit_orgs ||
				    ( is_logged_in() && $user_Organization->owner_user_ID == $current_User->ID ) ||
				    ! $curr_orgs[ $organization_ID ]['accepted'] )
				{	// Update priority if current user has permission or it is not accepted yet by admin
					$insert_orgs[ $organization_ID ]['priority'] = ( empty( $organization_priorities[ $o ] ) ? NULL : $organization_priorities[ $o ] );
				}
				else
				{	// Don't updated priority by current user
					$insert_orgs[ $organization_ID ]['priority'] = $curr_orgs[ $organization_ID ]['priority'];
				}
			}
			else
			{ // Insert user in new organization
				$insert_orgs[ $organization_ID ]['role'] = ( empty( $organization_roles[ $o ] ) ? NULL : $organization_roles[ $o ] );
				$insert_orgs[ $organization_ID ]['priority'] = ( empty( $organization_priorities[ $o ] ) ? NULL : $organization_priorities[ $o ] );
			}
		}

		if( count( $insert_orgs ) > 0 )
		{ // Insert new records with user-org relations
			$insert_org_SQL = 'REPLACE INTO T_users__user_org ( uorg_user_ID, uorg_org_ID, uorg_accepted, uorg_role, uorg_priority ) VALUES ';
			$o = 0;
			foreach( $insert_orgs as $insert_org_ID => $insert_org_data )
			{
				if( isset( $curr_orgs[ $insert_org_ID ] ) )
				{ // If we are updating - Don't change the accept status
					$insert_orgs_accepted = $curr_orgs[ $insert_org_ID ]['accepted'];
				}
				else
				{ // If we are inserting - Set the accept status depends on user perms or func params
					$insert_orgs_accepted = '0';
					if( $force_accept )
					{	// Force the accept status for this request:
						$insert_orgs_accepted = '1';
					}
					else
					{	// Check if it can be autoaccepted:
						$user_Organization = & $OrganizationCache->get_by_ID( $insert_org_ID );
						if( $user_Organization->can_be_autoaccepted() )
						{	// This organization can be autoaccepted:
							$insert_orgs_accepted = '1';
						}
					}
				}
				if( $o > 0 )
				{ // separator
					$insert_org_SQL .= ', ';
				}
				$insert_org_SQL .= '( '.$this->ID.', '.$DB->quote( $insert_org_ID ).', '.$insert_orgs_accepted.', '.$DB->quote( $insert_org_data['role'] ).', '.$DB->quote( $insert_org_data['priority'] ).' )';
				$o++;
			}
			$DB->query( $insert_org_SQL );
		}

		$delete_org_IDs = array_diff( $curr_org_IDs, array_keys( $insert_orgs ) );
		if( count( $delete_org_IDs ) > 0 )
		{ // Delete old records that were replaced with new
			$DB->query( 'DELETE FROM T_users__user_org
				WHERE uorg_user_ID = '.$this->ID.'
					AND uorg_org_ID IN ( '.implode( ', ', $delete_org_IDs ).' )' );
		}
	}


	/**
	 * Update user's secondary groups in DB
	 *
	 * @param array Secondary group IDs
	 */
	function update_secondary_groups()
	{
		global $DB, $current_User;

		if( ! is_logged_in() )
		{	// User must be logged in for this action:
			return;
		}

		if( ! $current_User->can_moderate_user( $this->ID ) )
		{	// Current User has no permission to moderate this User:
			return;
		}

		if( ! isset( $this->old_secondary_groups ) || $this->old_secondary_groups === false )
		{	// Skip a request without updating secondary groups:
			return;
		}

		// Check each old secondary group if it can be deleted by current user:
		$delete_old_secondary_groups = array();
		foreach( $this->old_secondary_groups as $old_secondary_Group )
		{
			if( $old_secondary_Group->can_be_assigned() )
			{	// Current user can delete only this group:
				$delete_old_secondary_group_IDs[] = $old_secondary_Group->ID;
			}
		}
		if( ! empty( $delete_old_secondary_group_IDs ) )
		{	// Clear secondary groups only which can be touched by currrent user:
			$DB->query( 'DELETE FROM T_users__secondary_user_groups
				WHERE sug_user_ID = '.$this->ID.'
					AND sug_grp_ID IN ( '.$DB->quote( $delete_old_secondary_group_IDs ).' )' );
		}
		// Set this var in order to don't update secondary group twice:
		$this->old_secondary_groups = false;

		if( ! empty( $this->secondary_groups ) )
		{	// Update new secondary groups:
			$new_secondary_grp_IDs = array();
			foreach( $this->secondary_groups as $secondary_Group )
			{
				if( $secondary_Group->can_be_assigned() )
				{	// We can add this secondary group because current user has a permission:
					if( $secondary_Group->ID != $this->get( 'grp_ID' ) &&
							! in_array( $secondary_Group->ID, $new_secondary_grp_IDs ) )
					{	// Add except of primary user group and the duplicates:
						$new_secondary_grp_IDs[] = $secondary_Group->ID;
					}
				}
			}

			if( ! empty( $new_secondary_grp_IDs ) )
			{	// Insert new secondary groups:
				$DB->query( 'INSERT INTO T_users__secondary_user_groups ( sug_user_ID, sug_grp_ID )
					VALUES ( '.$this->ID.', '.implode( ' ), ( '.$this->ID.', ', $new_secondary_grp_IDs ).' )' );
			}
		}
	}


	/**
	 * Get secondary groups
	 *
	 * @return array Secondary groups array of Group objects
	 */
	function get_secondary_groups()
	{
		if( ! is_array( $this->secondary_groups ) )
		{	// Initialize the secondary groups:
			if( $this->ID > 0 )
			{	// For existing user:
				global $DB;

				// Initialize SQL for secondary groups of this user:
				$secondary_groups_SQL = new SQL( 'Get secondary groups of the User #'.$this->ID );
				$secondary_groups_SQL->SELECT( '*' );
				$secondary_groups_SQL->FROM( 'T_groups' );
				$secondary_groups_SQL->FROM_add( 'INNER JOIN T_users__secondary_user_groups ON grp_ID = sug_grp_ID' );
				$secondary_groups_SQL->WHERE( 'sug_user_ID = '.$this->ID );

				// Load all secondary group objects of this user in cache:
				$GroupCache = & get_GroupCache();
				$GroupCache->clear();
				$this->secondary_groups = $GroupCache->load_by_sql( $secondary_groups_SQL );
			}
			else
			{	// For new creating user:
				$this->secondary_groups = array();
			}
		}

		return $this->secondary_groups;
	}


	/**
	 * Get primary & secondary user's groups
	 *
	 * @return array All user's groups array of Group objects
	 */
	function get_groups()
	{
		$primary_group = $this->get_Group();
		$secondary_groups = $this->get_secondary_groups();

		// Prepend primary group to secondary groups:
		array_unshift( $secondary_groups, $primary_group );

		return $secondary_groups;
	}


	/**
	 * Get user name depending on general setting 'username_display'
	 *
	 * @param string Output format, see {@link format_to_output()}
	 * @return string Login or preferred name
	 */
	function get_username( $format = NULL )
	{
		global $Settings;

		if( $Settings->get( 'username_display' ) == 'name' )
		{ // Get nickname or fullname
			$username = $this->get_preferred_name();
		}
		else
		{ // Get login
			$username = $this->get( 'login' );
		}

		if( ! is_null( $format ) )
		{ // Format output string
			$username = format_to_output( $username, $format );
		}

		return $username;
	}


	/**
	 * Calculate how much days user has after registration
	 *
	 * @return float
	 */
	function get_days_count_close()
	{
		$date_start = strtotime( $this->datecreated );
		$date_end = time();
		$days = ( $date_end - $date_start ) / 86400;

		return number_format( $days, 1, '.', '' );
	}


	/**
	 * Get count of own blogs of the user
	 *
	 * @return integer
	 */
	function get_own_blogs_count()
	{
		global $DB;

		$SQL = new SQL( 'Get a count of own collections of user #'.$this->ID );
		$SQL->SELECT( 'COUNT( blog_ID )' );
		$SQL->FROM( 'T_blogs' );
		$SQL->WHERE( 'blog_owner_user_ID = '.$DB->quote( $this->ID ) );

		return $DB->get_var( $SQL );
	}


	/**
	 * Check if user must accept terms & conditions
	 *
	 * @return boolean
	 */
	function must_accept_terms()
	{
		global $UserSettings, $Settings;

		if( ! $Settings->get( 'site_terms_enabled' ) )
		{	// The terms are not enabled:
			return false;
		}

		if( $UserSettings->get( 'terms_accepted', $this->ID ) )
		{	// This user already accepted the terms:
			return false;
		}

		// Get ID of page with terms & conditions from global settings:
		$terms_page_ID = intval( $Settings->get( 'site_terms' ) );

		$ItemCache = & get_ItemCache();
		if( $terms_page_ID && $terms_Item = & $ItemCache->get_by_ID( $terms_page_ID, false, false ) )
		{	// The post for terms is defined and user still must accept it:
			return true;
		}
		else
		{	// No terms for this site:
			return false;
		}
	}


	/**
	 * Get a count of flagged items by this user in current collection
	 *
	 * @param integer Collection ID, NULL to use current
	 * @return integer
	 */
	function get_flagged_items_count( $coll_ID = NULL )
	{
		if( ! is_logged_in() )
		{	// Only logged in users can have the flagged items:
			return 0;
		}

		if( $coll_ID === NULL )
		{	// Use current Collection:
			global $Collection, $Blog;
			$flagged_Blog = $Blog;
		}
		else
		{	// Use requested Collection:
			$BlogCache = & get_BlogCache();
			$flagged_Blog = & $BlogCache->get_by_ID( $coll_ID, false, false );
		}

		if( empty( $flagged_Blog ) )
		{	// Collection must be defined:
			return 0;
		}

		if( ! isset( $this->flagged_items_count ) )
		{	// Initialize array once:
			$this->flagged_items_count = array();
		}

		if( ! isset( $this->flagged_items_count[ $coll_ID ] ) )
		{	// Get it from DB only first time and then cache in var:
			$flagged_ItemList2 = new ItemList2( $flagged_Blog, $flagged_Blog->get_timestamp_min(), $flagged_Blog->get_timestamp_max(), NULL, 'ItemCache', 'flagged' );

			// Set additional debug info prefix for SQL queries in order to know what code executes it:
			$flagged_ItemList2->query_title_prefix = 'Flagged Items';

			// Filter only the flagged items:
			$flagged_ItemList2->set_default_filters( array(
					'flagged' => 1
				) );

			// Run query initialization to get total rows:
			$flagged_ItemList2->query_init();

			$this->flagged_items_count[ $coll_ID ] = $flagged_ItemList2->total_rows;
		}

		return $this->flagged_items_count[ $coll_ID ];
	}


	/**
	 * Get password driver
	 *
	 * @return object|FALSE Password driver OR FALSE if password driver is not detected for this user
	 */
	function get_PasswordDriver()
	{
		if( $this->PasswordDriver === NULL )
		{	// Initialize password driver:
			$this->PasswordDriver = get_PasswordDriver( $this->pass_driver );
		}

		return $this->PasswordDriver;
	}


	/**
	 * Get IDs List/Newsletters which are allowed for this User
	 *
	 * @return array Newsletter IDs
	 */
	function get_allowed_newsletter_IDs()
	{
		if( ! isset( $this->allowed_newsletter_IDs ) )
		{	// Load allowed newsletters from cache:
			global $DB, $current_User;
			$SQL = new SQL( 'Get allowed newsletters for User #'.$this->ID );
			$SQL->SELECT( 'enlt_ID' );
			$SQL->FROM( 'T_email__newsletter' );
			$SQL->WHERE( 'enlt_active = 1' );
			$perm_conditions = array( 'enlt_perm_subscribe = "anyone"' );
			$check_groups = array();
			if( check_user_perm( 'users', 'edit' ) )
			{	// Allow to subscribe to forbidden newsletters by user admins:
				$perm_conditions[] = 'enlt_perm_subscribe = "admin"';
				$check_groups[] = $current_User->get( 'grp_ID' );
			}
			$check_groups[] = $this->get( 'grp_ID' );
			$check_groups = count( $check_groups ) > 1 ? '('.implode( '|', $check_groups ).')' : $check_groups[0];
			$perm_conditions[] = 'enlt_perm_subscribe = "group" AND enlt_perm_groups REGEXP( "(^|,)'.$check_groups.'(,|$)" )';
			$SQL->WHERE_and( implode( ' OR ', $perm_conditions ) );
			$this->allowed_newsletter_IDs = $DB->get_col( $SQL );
		}

		return $this->allowed_newsletter_IDs;
	}


	/**
	 * Get List/Newsletters which are allowed for this User
	 *
	 * @return array Newsletter objects
	 */
	function get_allowed_newsletters()
	{
		if( ! isset( $this->allowed_newsletters ) )
		{	// Load allowed newsletters from cache:
			$newsletter_IDs = $this->get_allowed_newsletter_IDs();
			$this->allowed_newsletters = array();
			if( count( $newsletter_IDs ) > 0 )
			{
				$NewsletterCache = & get_NewsletterCache();
				$NewsletterCache->load_list( $newsletter_IDs );
				foreach( $newsletter_IDs as $newsletter_ID )
				{
					if( $Newsletter = & $NewsletterCache->get_by_ID( $newsletter_ID, false, false ) )
					{
						$this->allowed_newsletters[ $Newsletter->ID ] = $Newsletter;
					}
				}
			}
		}

		return $this->allowed_newsletters;
	}


	/**
	 * Check if the newsletter is allowed for this User
	 *
	 * @param integer Newsletter ID
	 * @return boolean
	 */
	function is_allowed_newsletter( $newsletter_ID )
	{
		return in_array( $newsletter_ID, $this->get_allowed_newsletter_IDs() );
	}


	/**
	 * Get IDs of newsletters which this user is subscribed on
	 *
	 * @param string Type: 'subscribed', 'unsubscribed', 'all'
	 * @return array
	 */
	function get_newsletter_subscriptions( $type = 'subscribed' )
	{
		if( empty( $this->ID ) )
		{	// Only created user can has the newsletter subscriptions:
			return array();
		}

		if( ! isset( $this->newsletter_subscriptions ) )
		{	// Initialize newsletter subscriptions array only on first request:
			global $DB;

			$this->newsletter_subscriptions = array(
					'subscribed'   => array(),
					'unsubscribed' => array(),
				);

			$SQL = new SQL( 'Get list subscriptions of user #'.$this->ID );
			$SQL->SELECT( 'enls_enlt_ID, enls_subscribed' );
			$SQL->FROM( 'T_email__newsletter_subscription' );
			$SQL->WHERE( 'enls_user_ID = '.$this->ID );
			$newsletter_subscriptions = $DB->get_assoc( $SQL );
			foreach( $newsletter_subscriptions as $newsletter_ID => $is_subscribed )
			{
				if( $is_subscribed )
				{
					$this->newsletter_subscriptions['subscribed'][] = $newsletter_ID;
				}
				else
				{
					$this->newsletter_subscriptions['unsubscribed'][] = $newsletter_ID;
				}
			}
		}

		if( $type == 'all' )
		{
			return array_merge( $this->newsletter_subscriptions['subscribed'], $this->newsletter_subscriptions['unsubscribed'] );
		}
		else
		{
			return $this->newsletter_subscriptions[ $type ];
		}
	}


	/**
	 * Check if this user is subscribed to requested newsletter
	 *
	 * @param integer Newsletter ID
	 * @return boolean
	 */
	function is_subscribed( $newsletter_ID )
	{
		return in_array( $newsletter_ID, $this->get_newsletter_subscriptions() );
	}


	/**
	 * Set newsletter subscriptions for this user
	 *
	 * @param array
	 * @param array Optional subscription params. This will be applied to the next call to User::subscribe() and cleared thereafter.
	 *              This will also be overridden by params passed to User::subscribe().
	 *              - 'usertags': Comma-delimited user tags applied as part of subscription
	 */
	function set_newsletter_subscriptions( $new_subscriptions, $params = array() )
	{
		$prev_subscriptions = $this->get_newsletter_subscriptions();

		if( ! is_array( $new_subscriptions ) )
		{	// This must be an array:
			$new_subscriptions = array();
		}

		if( count( $new_subscriptions ) > 0 )
		{
			foreach( $new_subscriptions as $n => $new_subscription_ID )
			{
				// Format each value to integer:
				$new_subscription_ID = intval( $new_subscription_ID );
				if( empty( $new_subscription_ID ) ||
				    ( ! $this->is_allowed_newsletter( $new_subscription_ID ) &&
				      ! in_array( $new_subscription_ID, $prev_subscriptions ) ) )
				{	// Unset wrong value or
					//   if current User cannot subscribe this User to the new newsletter
					//   but keep if it was subscribed before:
					unset( $new_subscriptions[ $n ] );
				}
				else
				{	// Keep integer values only:
					$new_subscriptions[ $n ] = $new_subscription_ID;
				}
			}
		}

		// Check if subscriptions were updated:
		if( count( $prev_subscriptions ) != count( $new_subscriptions ) )
		{	// If a count of subscriptions are not equals then mark they are updated:
			$this->newsletter_subscriptions_updated = true;
		}
		else
		{
			sort( $new_subscriptions );
			sort( $prev_subscriptions );
			if( $prev_subscriptions != $new_subscriptions )
			{	// If subscriptions were really updated:
				$this->newsletter_subscriptions_updated = true;
			}
		}

		// Set new subscriptions:
		$this->new_newsletter_subscriptions = $new_subscriptions;

		// Set newsletter subscription params:
		$this->newsletter_subscription_params = $params;
	}


	/**
	 * Subscribe to newsletter
	 *
	 * @param array|integer Newsletter IDs
	 * @param array Optional subscription params
	 *              - 'usertags': Comma-delimited user tags applied as part of subscription
	 * @return integer|boolean # of rows affected or false if error
	 */
	function subscribe( $newsletter_IDs, $params = array() )
	{
		global $DB, $localtimenow, $servertimenow, $current_User;

		if( empty( $this->ID ) )
		{	// Only created user can has the newsletter subscriptions:
			return false;
		}

		if( empty( $newsletter_IDs ) )
		{	// No subscriptions:
			return false;
		}

		$DB->begin();

		if( ! is_array( $newsletter_IDs ) )
		{	// Convert single newsletter ID to array:
			$newsletter_IDs = array( $newsletter_IDs );
		}

		// Decide what newsletter subscriptions should be inserted into DB and what should be updated:
		$newsletter_subscriptions = $this->get_newsletter_subscriptions( 'all' );
		$insert_newsletter_IDs = array_diff( $newsletter_IDs, $newsletter_subscriptions );
		$update_newsletter_IDs = array_intersect( $newsletter_IDs, $newsletter_subscriptions );

		// Make sure we get a list newletters IDs for resubscriptions:
		$resubscribe_newsletter_IDs = array_intersect( $newsletter_IDs, $this->get_newsletter_subscriptions( 'unsubscribed' ) );

		$r = 0;

		if( count( $insert_newsletter_IDs ) )
		{	// If new records should be inserted:
			$insert_newsletter_sql_values = array();
			foreach( $insert_newsletter_IDs as $insert_newsletter_ID )
			{
				$insert_newsletter_sql_values[] = '( '.$DB->quote( $this->ID ).', '.$DB->quote( $insert_newsletter_ID ).', 1, '.$DB->quote( date2mysql( $localtimenow ) ).' )';
			}
			$r += $DB->query( 'INSERT INTO T_email__newsletter_subscription ( enls_user_ID, enls_enlt_ID, enls_subscribed, enls_subscribed_ts )
				VALUES '.implode( ', ', $insert_newsletter_sql_values ),
				'Subscribe(insert new subscriptions) user #'.$this->ID.' to lists #'.implode( ',', $insert_newsletter_IDs ) );

			// Send emails of campaigns which must be sent at subscription:
			$this->send_auto_subscriptions( $insert_newsletter_IDs );
		}

		if( count( $update_newsletter_IDs ) )
		{	// If previous unsubscriptions should be subscribed again:
			$r += $DB->query( 'UPDATE T_email__newsletter_subscription
			SET enls_subscribed = 1,
			    enls_subscribed_ts = '.$DB->quote( date2mysql( $localtimenow ) ).'
			WHERE enls_user_ID = '.$DB->quote( $this->ID ).'
			  AND enls_enlt_ID IN ( '.$DB->quote( $update_newsletter_IDs ).' )
			  AND enls_subscribed = 0',
			'Subscribe(update unsubscriptions) user #'.$this->ID.' to lists #'.implode( ',', $update_newsletter_IDs ) );
		}

		if( count( $insert_newsletter_IDs ) || count( $resubscribe_newsletter_IDs ) )
		{	// Notify list owner of new subscriber:
			$email_template_params = array_merge( array(
				'subscribed_User'     => $this,
				'subscribed_by_admin' => ( is_logged_in() && $current_User->login != $this->login ? $current_User->login : '' ),
			), empty( $this->newsletter_subscription_params ) ? array() : $this->newsletter_subscription_params, $params );

			// Add to changes in subscription values:
			$this->newsletter_subscription_changed_values[] = array( 'subscribe', array_unique( array_merge( $insert_newsletter_IDs, $resubscribe_newsletter_IDs ) ), $email_template_params );
		}

		// Insert user states for newsletters with automations:
		$first_step_SQL = new SQL( 'Get first step of automation' );
		$first_step_SQL->SELECT( 'step_ID' );
		$first_step_SQL->FROM( 'T_automation__step' );
		$first_step_SQL->WHERE( 'step_autm_ID = aunl_autm_ID' );
		$first_step_SQL->ORDER_BY( 'step_order ASC' );
		$first_step_SQL->LIMIT( 1 );
		$automations_SQL = new SQL( 'Get automations of the subscribed newsletters' );
		$automations_SQL->SELECT( 'DISTINCT aunl_autm_ID, '.$this->ID.', ( '.$first_step_SQL->get().' ), '.$DB->quote( date2mysql( $servertimenow ) ) );
		$automations_SQL->FROM( 'T_email__newsletter' );
		$automations_SQL->FROM_add( 'INNER JOIN T_automation__newsletter ON enlt_ID = aunl_enlt_ID' );
		$automations_SQL->FROM_add( 'LEFT JOIN T_automation__user_state ON aust_autm_ID = aunl_autm_ID AND aust_user_ID = '.$this->ID );
		$automations_SQL->WHERE( 'enlt_ID IN ( '.$DB->quote( $newsletter_IDs ).' )' );
		$automations_SQL->WHERE_and( 'aunl_autostart = 1' );
		$automations_SQL->WHERE_and( 'aust_autm_ID IS NULL' );// Exclude already added automation user states
		$DB->query( 'INSERT INTO T_automation__user_state ( aust_autm_ID, aust_user_ID, aust_next_step_ID, aust_next_exec_ts ) '.$automations_SQL->get(),
			'Insert automation user states on subscribe to newsletters #'.implode( ',', $newsletter_IDs ).' for user #'.$this->ID );

		$DB->commit();

		// Unset flag in order not to run this twice:
		$this->newsletter_subscriptions_updated = false;

		// Unset newsletter subscription params for subsequent calls to this function:
		unset( $this->newsletter_subscription_params );

		// Update the CACHE array where we store who are subscribed already in order to avoid a duplicate entry error on second calling of this function:
		$this->newsletter_subscriptions['subscribed'] = array_merge( $this->newsletter_subscriptions['subscribed'], $newsletter_IDs );
		$this->newsletter_subscriptions['unsubscribed'] = array_diff( $this->newsletter_subscriptions['unsubscribed'], $newsletter_IDs );

		return $r;
	}


	/**
	 * Unsubscribe from newsletter
	 *
	 * @param array|integer Newsletter IDs
	 * @param array Optional subscription params
	 *              - 'usertags': Comma-delimited user tags applied as part of unsubscription
	 * @return integer|boolean # of rows affected or false if error
	 */
	function unsubscribe( $newsletter_IDs, $params = array() )
	{
		global $DB, $localtimenow, $current_User;

		if( empty( $this->ID ) )
		{	// Only created user can has the newsletter subscriptions:
			return false;
		}

		if( empty( $newsletter_IDs ) )
		{	// No subscriptions:
			return false;
		}

		if( ! is_array( $newsletter_IDs ) )
		{	// Convert single newsletter ID to array:
			$newsletter_IDs = array( $newsletter_IDs );
		}

		// Get list of newsletter IDs that will be updated:
		$newsletter_subscriptions = $this->get_newsletter_subscriptions( 'subscribed' );
		$update_newsletter_IDs = array_intersect( $newsletter_IDs, $newsletter_subscriptions );

		$r = $DB->query( 'UPDATE T_email__newsletter_subscription
			SET enls_subscribed = 0,
			    enls_unsubscribed_ts = '.$DB->quote( date2mysql( $localtimenow ) ).'
			WHERE enls_user_ID = '.$DB->quote( $this->ID ).'
			  AND enls_enlt_ID IN ( '.$DB->quote( $newsletter_IDs ).' )
			  AND enls_subscribed = 1',
			'Unsubscribe user #'.$this->ID.' from lists #'.implode( ',', $newsletter_IDs ) );

		if( $r )
		{	// If user has been unsubscribed from at least one newsletter,
			// Then this user must automatically exit all automations tied to those newsletters:
			$DB->query( 'DELETE T_automation__user_state FROM T_automation__user_state
				INNER JOIN T_automation__newsletter ON aunl_autm_ID = aust_autm_ID
				WHERE aust_user_ID = '.$DB->quote( $this->ID ).'
				  AND aunl_enlt_ID IN ( '.$DB->quote( $newsletter_IDs ).' )
				  AND aunl_autoexit = 1',
				'Exit user automatically from all automations tied to lists #'.implode( ',', $newsletter_IDs ) );
		}

		if( count( $update_newsletter_IDs ) )
		{	// Notify list owner of lost subscriber:
			$email_template_params = array_merge( array(
				'subscribed_User'       => $this,
				'unsubscribed_by_admin' => ( is_logged_in() && $current_User->login != $this->login ? $current_User->login : '' ),
			), empty( $this->newsletter_unsubscription_params ) ? array() : $this->newsletter_unsubscription_params, $params );

			// Add to changes in subscription values:
			$this->newsletter_subscription_changed_values[] = array( 'unsubscribe', $update_newsletter_IDs, $email_template_params );
		}

		// Unset newsletter unsubscription params for subsequent calls to this function:
		unset( $this->newsletter_unsubscription_params );

		// Update the CACHE array where we store who are subscribed already in order to avoid a duplicate entry error on second calling of this function:
		$this->newsletter_subscriptions['subscribed'] = array_diff( $this->newsletter_subscriptions['subscribed'], $update_newsletter_IDs );
		$this->newsletter_subscriptions['unsubscribed'] = array_unique( array_merge( $this->newsletter_subscriptions['unsubscribed'], $update_newsletter_IDs ) );

		return $r;
	}


	/**
	 * Send emails of campaign which must be sent at subscription
	 *
	 * @param array Newsletters IDs
	 */
	function send_auto_subscriptions( $newsletter_IDs )
	{
		global $DB;

		// Additional check to know what newsletter is really new, in order to exclude the updating subscriptions:
		$newsletter_subscriptions = $this->get_newsletter_subscriptions( 'all' );
		$new_subscriptions = array_diff( $newsletter_IDs, $newsletter_subscriptions );

		if( count( $new_subscriptions ) )
		{	// User is really subscribing to new newsletters:
			$EmailCampaignCache = & get_EmailCampaignCache();
			$EmailCampaignCache->clear();
			$EmailCampaignCache->load_where( 'ecmp_enlt_ID IN ( '.$DB->quote( $new_subscriptions ).' ) AND ecmp_welcome = 1' );

			foreach( $EmailCampaignCache->cache as $EmailCampaign )
			{	// Send a "Welcome" email of the campaign at subscription:
				$r = $EmailCampaign->send_all_emails( false, array( $this->ID ), 'welcome' );
				if( $r && $EmailCampaign->get( 'activate' ) )
				{	// Set a flag to know email campaign which is used as "Activate" message has been sent now for this User:
					$this->welcome_activate_email_campaign_sent = true;
				}
			}
		}
	}


	/**
	 * Send email notification to owners of lists where user subscribed/unsubscribed
	 *
	 * @param string subscribed | unsubscribed | all
	 */
	function send_list_owner_notifications( $filter = 'all' )
	{
		if( ! empty( $this->newsletter_subscription_changed_values ) )
		{
			foreach( $this->newsletter_subscription_changed_values as $key => $value )
			{
				list( $action, $newsletter_IDs, $email_template_params ) = $value;
				if( $filter == 'all' || $filter == $action )
				{
					switch( $action )
					{
						case 'subscribe':
							$template_name = 'list_new_subscriber';
							break;

						case 'unsubscribe':
							$template_name = 'list_lost_subscriber';
							break;

						default:
							debug_die( 'Invalid type of subscription action' );
					}

					// Send an email to the list owner:
					send_list_owner_notification( $newsletter_IDs, $template_name, $email_template_params );

					// Unset subscription change:
					unset( $this->newsletter_subscription_changed_values[$key] );
				}
			}
		}
	}


	/**
	 * Retrieves all tags assigned to user
	 *
	 * @return array of tags
	 */
	function get_usertags()
	{
		if( ! isset( $this->user_tags ) )
		{
			global $DB;

			$tags_SQL = new SQL( 'Get tags' );
			$tags_SQL->SELECT( 'utag_name' );
			$tags_SQL->FROM( 'T_users__tag' );
			$tags_SQL->FROM_add( 'INNER JOIN T_users__usertag ON uutg_emtag_ID = utag_ID' );
			$tags_SQL->WHERE( 'uutg_user_ID = '.$DB->quote( $this->ID ) );

			$this->user_tags = $DB->get_col( $tags_SQL );
		}

		return $this->user_tags;
	}


	/**
	 * Get count of user's profile visitors
	 *
	 * @param boolean True to only count new unique profile visitors since the user viewed the list, False if total unique profile visitors
	 * @return integer Visitor count
	 */
	function get_profile_visitors_count( $new_only = true )
	{
		global $DB;

		$count = $DB->get_var( 'SELECT '.( $new_only ? 'upvc_new_unique_visitors' : 'upvc_total_unique_visitors' ).' FROM T_users__profile_visit_counters WHERE upvc_user_ID = '.$this->ID );

		if( is_null( $count ) )
		{
			return 0;
		}

		return $count;
	}


	/**
	 * Save tags to DB
	 *
	 * @param string 'insert' | 'update'
	 */
	function insert_update_user_tags( $mode )
	{
		global $DB;

		if( isset( $this->user_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_users__usertag
											WHERE uutg_user_ID = '.$this->ID, 'delete previous tags' );
			}

			if( ! empty( $this->user_tags ) )
			{
				// Find the tags that are already in the DB
				$query = 'SELECT utag_name
										FROM T_users__tag
									 WHERE utag_name IN ('.$DB->quote( $this->user_tags ).')';
				$existing_tags = $DB->get_col( $query, 0, 'Find existing tags' );

				$new_tags = array_diff( $this->user_tags, $existing_tags );

				if( ! empty( $new_tags ) )
				{ // insert new tags:
					$query = "INSERT INTO T_users__tag( utag_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_users__usertag( uutg_user_ID, uutg_emtag_ID )
								  SELECT '.$this->ID.', utag_ID
									  FROM T_users__tag
									 WHERE utag_name IN ('.$DB->quote( $this->user_tags ).')';
				$DB->query( $query, 'Make tag associations!' );
			}

			$DB->commit();
		}
	}


	/**
	 * Split tags by comma or semicolon
	 *
	 * @param string The tags, separated by comma or semicolon
	 */
	function set_usertags_from_string( $user_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['user_tags'] = true;

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

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

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


	/**
	 * Append user tags
	 *
	 * @param string/array comma separated tags or array of tags
	 */
	function add_usertags( $user_tags )
	{
		if( empty( $user_tags ) )
		{
			return;
		}

		if( ! is_array( $user_tags ) )
		{
			$user_tags = preg_split( '/\s*[;,]+\s*/', $user_tags, -1, PREG_SPLIT_NO_EMPTY );
		}

		$this->get_usertags();
		foreach( $user_tags as $t => $user_tag )
		{
			// Replace ; and , in case $tags parameter was passed as an array
			$user_tag = str_replace( array( ';', ',' ), ' ', $user_tag );

			if( substr( $user_tag, 0, 1 ) == '-' )
			{ // Prevent chars '-' in first position
				$user_tag = preg_replace( '/^-+/', '', $user_tag );
			}
			if( empty( $user_tag ) )
			{ // Don't save empty tag
				unset( $user_tags[ $t ] );
			}
			else
			{ // Append the tag and mark that tags has been updated
				$this->dbchanges_flags['user_tags'] = true;
				$this->user_tags[] = $user_tag;
			}
		}

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


	/**
	 * Remove user tags
	 *
	 * @param string/array comma separated tags or array of tags
	 */
	function remove_usertags( $user_tags )
	{
		if( empty( $user_tags ) )
		{
			return;
		}

		if( ! is_array( $user_tags ) )
		{
			$user_tags = preg_split( '/\s*[;,]+\s*/', $user_tags, -1, PREG_SPLIT_NO_EMPTY );
		}

		$this->get_usertags();

		foreach( $user_tags as $t => $user_tag )
		{
			if( empty( $user_tag ) )
			{	// Skip empty tag:
				continue;
			}
			elseif( ( $user_tag_index = array_search( $user_tag, $this->user_tags ) ) !== false )
			{	// Remove user tag from array:
				$this->dbchanges_flags['user_tags'] = true;
				unset( $this->user_tags[ $user_tag_index ] );
			}
		}
	}
}

?>
