<?php
/**
 * This file implements the UserList class.
 *
 * This file is part of the b2evolution/evocms project - {@link http://b2evolution.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-2005 by Daniel HAHLER - {@link http://thequod.de/contact}.
 *
 * @package evocore
 */
if( !defined('EVO_MAIN_INIT') ) die( 'Please, do not access this page directly.' );

load_class( '_core/model/dataobjects/_dataobjectlist2.class.php', 'DataObjectList2' );
load_class( 'users/model/_userquery.class.php', 'UserQuery' );

/**
 * UserList Class
 *
 * @package evocore
 */
class UserList extends DataObjectList2
{
	/**
	 * SQL object for the Query
	 */
	var $UserQuery;

	/**
	 * Boolean var, Set TRUE when we should to get new user IDs from DB (when user changes the filter params)
	 */
	var $refresh_query = false;

	/**
	 * Boolean var, TRUE - to memorize params (regenerate_url)
	 */
	var $memorize = true;

	/**
	 * @var array Params to build query
	 */
	var $query_params = array();

	/**
	 * Exclude users by ID after the list was already filtered, @see run_query()
	 * @var string separated by comma
	 */
	var $exclude_users = NULL;

	/**
	 * Constructor
	 *
	 * @param integer|NULL Limit
	 * @param string prefix to differentiate page/order params when multiple Results appear one same page
	 * @param string Name to be used when saving the filterset (leave empty to use default)
	 * @param array Query params:
	 *                    'join_group'   => true,
	 *                    'join_session' => false,
	 *                    'join_country'   => true,
	 *                    'join_region'    => false,
	 *                    'join_subregion' => false,
	 *                    'join_city'      => true,
	 *                    'join_colls'     => true,
	 *                    'keywords_fields'     - Fields of users table to search by keywords
	 *                    'where_status_closed' - FALSE - to don't display closed users
	 *                    'where_org_ID' - ID of organization
	 *                    'order_by_login_length' => 'A' for increasing login length, 'D' for decreasing login length
	 */
	function __construct(
		$filterset_name = '', // Name to be used when saving the filterset (leave empty to use default)
		$limit = 20, // Page size
		$param_prefix = 'users_',
		$query_params = array()
		)
	{
		// Call parent constructor:
		parent::__construct( get_Cache( 'UserCache' ), $limit, $param_prefix, NULL );

		// Init query params, @see $this->query_init()
		$this->query_params = $query_params;

		if( !empty( $filterset_name ) )
		{	// Set the filterset_name with the filterset_name param
			$this->filterset_name = 'UserList_filters_'.preg_replace( '#[^a-z0-9\-_]#i', '', $filterset_name );
		}
		else
		{	// Set a generic filterset_name
			$this->filterset_name = 'UserList_filters';
		}

		$this->order_param = 'results_'.$this->param_prefix.'order';
		$this->page_param = $this->param_prefix.'paged';

		// Initialize the default filter set:
		$this->set_default_filters( array(
				'filter_preset'       => NULL,
				'filter_query'        => '',    // string, Query in JSON format generated by jQuery plugin QueryBuilder
				'country'             => NULL,    // integer, Country ID
				'region'              => NULL,    // integer, Region ID
				'subregion'           => NULL,    // integer, Subregion ID
				'city'                => NULL,    // integer, City ID
				'membersonly'         => false,   // boolean, Restrict by members
				'keywords'            => NULL,    // string, Search words
				'email'               => NULL,    // string, Email
				'gender'              => NULL,    // string: 'M', 'F', 'O', 'MF', 'MO', 'FO' or 'MFO'
				'status_activated'    => NULL,    // string: 'activated'
				'account_status'      => NULL,    // string: 'new', 'activated', 'manualactivated', 'autoactivated', 'emailchanged', 'deactivated', 'failedactivation', 'pendingdelete', 'closed'
				'registered_min'      => '',    // date: Registered date from
				'registered_max'      => '',    // date: Registered date to
				'reported'            => NULL,    // integer: 1 to show only reported users
				'custom_sender_email' => NULL,    // integer: 1 to show only users with custom notifcation sender email address
				'custom_sender_name'  => NULL,    // integer: 1 to show only users with custom notifaction sender name
				'group'               => -1,      // integer: Primary user group ID, -1 = all groups but list is ungrouped, 0 - all groups with grouped list
				'group2'              => -1,      // integer: Secondary user group ID, -1 = all groups but list is ungrouped, 0 - all groups with grouped list
				'age_min'             => NULL,    // integer, Age min
				'age_max'             => NULL,    // integer, Age max
				'order'               => '/user_lastseen_ts/D',    // Order
				'users'               => array(), // User IDs - used to cache results
				'userids'             => array(), // User IDs - used to filter by user IDs
				'level_min'           => NULL,    // integer, Level min
				'level_max'           => NULL,    // integer, Level max
				'org'                 => NULL,    // integer, Organization ID
				'newsletter'          => NULL,    // integer, Newsletter ID, users which are subscribed to newsletter
				'not_newsletter'      => NULL,    // integer, Newsletter ID, users which are NOT subscribed to newsletter
				'newsletter_subscribed' => 1,     // 1 - only users with active subscription, 0 - only unsubscribed users, NULL - both
				'ecmp'                => NULL,    // integer, Email Campaign ID
				'recipient_type'      => NULL,    // string, Recipient type of email campaign: 'filtered', 'sent', 'readytosend'
				'recipient_action'    => NULL,    // string, Recipient action on email campaign: 'img_loaded', 'link_clicked', 'cta1', 'cta2', 'cta3', 'liked', 'disliked', 'clicked_unsubscribe'
				'user_tag'            => NULL,    // string, User tag
				'not_user_tag'        => NULL,    // string, User tag
		),
		// Preset filters:
		array(
			'men'       => array( 'gender' => 'M' ),
			'women'     => array( 'gender' => 'F' ),
			'other'     => array( 'gender' => 'O' ),
			'activated' => array( 'status_activated' => 1 ),
			'reported'  => array( 'reported' => 1 ),
		) );
	}


	/**
	 * Reset the query -- EXPERIMENTAL
	 *
	 * Useful to requery with a slighlty moidified filterset
	 */
	function reset()
	{
		// The SQL Query object:
		$this->UserQuery = new UserQuery( $this->Cache->dbtablename, $this->Cache->dbprefix, $this->Cache->dbIDname );

		parent::reset();
	}


	/**
	 * Set/Activate filterset
	 *
	 * This will also set back the GLOBALS !!! needed for regenerate_url().
	 *
	 * @param array Filters
	 */
	function set_filters( $filters )
	{
		if( !empty( $filters ) )
		{ // Activate the filterset (fallback to default filter when a value is not set):
			$this->filters = array_merge( $this->default_filters, $filters );
		}

		// Activate preset filters if necessary:
		$this->activate_preset_filters();

		// Page
		$this->page = param( $this->page_param, 'integer', 1 );

		// Country
		if( has_cross_country_restriction( 'users', 'list' ) )
		{ // In case of cross country restrionction we always have to set the ctry filter
			// In this case we always have a logged in user
			global $current_User;
			if( ( ! empty( $current_User->ctry_ID ) ) && ( $current_User->ctry_ID != $this->filters['country'] ) )
			{ // current country filter is not the same
				$this->filters['country'] = $current_User->ctry_ID;
				$this->refresh_query = true;
			}
		}

		// asimo> memorize is always false for now, because is not fully implemented
		if( $this->memorize )
		{	// set back the GLOBALS !!! needed for regenerate_url() :

			/*
			 * Selected filter preset:
			 */
			memorize_param( $this->param_prefix.'filter_preset', 'string', $this->default_filters['filter_preset'], $this->filters['filter_preset'] );

			/*
			 * Selected filter query:
			 */
			memorize_param( 'filter_query', 'string', $this->default_filters['filter_query'], $this->filters['filter_query'] );

			/*
			 * Restrict by user IDs
			 */
			memorize_param( 'userids', 'array:integer', $this->default_filters['userids'], $this->filters['userids'] );

			/*
			 * Restrict by membersonly
			 */
			memorize_param( 'membersonly', 'integer', $this->default_filters['membersonly'], $this->filters['membersonly'] );

			/*
			 * Restrict by keywords
			 */
			memorize_param( 'keywords', 'string', $this->default_filters['keywords'], $this->filters['keywords'] );			 // Search string

			/*
			 * Restrict by email
			 */
			memorize_param( 'email', 'string', $this->default_filters['email'], $this->filters['email'] );

			/*
			 * Restrict by gender
			 */
			memorize_param( 'gender_men', 'integer', strpos( $this->default_filters['gender'], 'M' ) !== false, strpos( $this->filters['gender'], 'M' ) !== false );
			memorize_param( 'gender_women', 'integer', strpos( $this->default_filters['gender'], 'F' ) !== false, strpos( $this->filters['gender'], 'F' ) !== false );
			memorize_param( 'gender_other', 'integer', strpos( $this->default_filters['gender'], 'O' ) !== false, strpos( $this->filters['gender'], 'O' ) !== false );

			/*
			 * Restrict by status
			 */
			memorize_param( 'status_activated', 'string', $this->default_filters['status_activated'], $this->filters['status_activated'] );
			memorize_param( 'account_status', 'string', $this->default_filters['account_status'], $this->filters['account_status'] );

			/*
			 * Restrict by registration date
			 */
			memorize_param( 'registered_min', 'date', $this->default_filters['registered_min'], $this->filters['registered_min'] );
			memorize_param( 'registered_max', 'date', $this->default_filters['registered_max'], $this->filters['registered_max'] );

			/*
			 * Restrict by reported state ( was reported or not )
			 */
			memorize_param( 'reported', 'integer', $this->default_filters['reported'], $this->filters['reported'] );

			/*
			 * Restrict by custom sender email settings
			 */
			memorize_param( 'custom_sender_email', 'integer', $this->default_filters['custom_sender_email'], $this->filters['custom_sender_email'] );
			memorize_param( 'custom_sender_name', 'integer', $this->default_filters['custom_sender_name'], $this->filters['custom_sender_name'] );

			/*
			 * Restrict by primary user group:
			 */
			memorize_param( 'group', 'integer', $this->default_filters['group'], $this->filters['group'] );

			/*
			 * Restrict by secondary user group:
			 */
			memorize_param( 'group2', 'integer', $this->default_filters['group2'], $this->filters['group2'] );

			/*
			 * Restrict by locations
			 */
			memorize_param( 'country', 'integer', $this->default_filters['country'], $this->filters['country'] );       // Search country
			memorize_param( 'region', 'integer', $this->default_filters['region'], $this->filters['region'] );          // Search region
			memorize_param( 'subregion', 'integer', $this->default_filters['subregion'], $this->filters['subregion'] ); // Search subregion
			memorize_param( 'city', 'integer', $this->default_filters['city'], $this->filters['city'] );                // Search city

			/*
			 * Restrict by age group
			 */
			memorize_param( 'age_min', 'integer', $this->default_filters['age_min'], $this->filters['age_min'] );
			memorize_param( 'age_max', 'integer', $this->default_filters['age_max'], $this->filters['age_max'] );

			/*
			 * Restrict by organization
			 */
			memorize_param( 'org', 'integer', $this->default_filters['org'], $this->filters['org'] );

			/*
			 * Restrict by newsletter
			 */
			memorize_param( 'newsletter', 'integer', $this->default_filters['newsletter'], $this->filters['newsletter'] );

			/*
			 * Restrict by not subscribed newsletter
			 */
			memorize_param( 'not_newsletter', 'integer', $this->default_filters['not_newsletter'], $this->filters['not_newsletter'] );

			/*
			 * Restrict by newsletter subscription activity
			 */
			memorize_param( 'newsletter_subscribed', 'integer', $this->default_filters['newsletter_subscribed'], $this->filters['newsletter_subscribed'] );

			/*
			 * Restrict by email campaign
			 */
			memorize_param( 'ecmp', 'integer', $this->default_filters['ecmp'], $this->filters['ecmp'] );

			/*
			 * Restrict by recipient type of email campaign
			 */
			memorize_param( 'recipient_type', 'string', $this->default_filters['recipient_type'], $this->filters['recipient_type'] );

			/*
			 * Restrict by recipient action on email campaign
			 */
			memorize_param( 'recipient_action', 'string', $this->default_filters['recipient_action'], $this->filters['recipient_action'] );

			/*
			 * Restrict by user tag
			 */
			memorize_param( 'user_tag', 'string', $this->default_filters['user_tag'], $this->filters['user_tag'] );

			/*
			 * Restrict by user tag
			 */
			memorize_param( 'not_user_tag', 'string', $this->default_filters['not_user_tag'], $this->filters['not_user_tag'] );


			/*
			 * order:
			 */
			$order = param( $this->order_param, 'string', '' );
			$this->order = $order != '' ? $order : $this->filters['order'];
			if( $this->order != $this->filters['order'] )
			{	// Save order from request
				$this->filters['order'] = $this->order;
				$this->save_filterset();
				$this->refresh_query = true;
			}
			memorize_param( $this->order_param, 'string', $this->default_filters['order'], $this->order ); // Order

			// 'paged'
			memorize_param( $this->page_param, 'integer', 1, $this->page ); // List page number in paged display
		}
	}


	/**
	 * Init filter params from request params
	 *
	 * @param boolean do we want to use saved filters ?
	 * @return boolean true if we could apply a filterset based on Request params (either explicit or reloaded)
	 */
	function load_from_Request( $use_filters = true )
	{
		$this->filters = $this->default_filters;

		if( $use_filters )
		{
			// Do we want to restore filters or do we want to create a new filterset
			$filter = param( 'filter', 'string', '' );
			switch( $filter )
			{
				case 'new':
					$this->refresh_query = true;
					break;

				case 'reset':
					// We want to reset the memorized filterset:
					global $Session;
					$Session->delete( $this->filterset_name );

					// Memorize global variables:
					$this->set_filters( array() );
					$this->refresh_query = true;
					// We have applied no filterset:
					return false;
					/* BREAK */

				case 'refresh':
					$this->refresh_query = true;
					return $this->restore_filterset();

				default:
					return $this->restore_filterset();
			}

			/**
			 * Filter preset
			 */
			$this->filters['filter_preset'] = param( $this->param_prefix.'filter_preset', 'string', $this->default_filters['filter_preset'], true );

			// Activate preset default filters if necessary:
			$this->activate_preset_filters();
		}

		/**
		 * Filter query
		 */
		$this->filters['filter_query'] = param_condition( 'filter_query', $this->default_filters['filter_query'], true );

		/*
		 * Restrict by user IDs
		 */
		$this->filters['userids'] = param( 'userids', 'array:integer', $this->default_filters['userids'], true );

		/*
		 * Restrict by members
		 */
		$this->filters['membersonly'] = param( 'membersonly', 'boolean', $this->default_filters['membersonly'], true );

		/*
		 * Restrict by keywords
		 */
		$this->filters['keywords'] = param( 'keywords', 'string', $this->default_filters['keywords'], true );         // Search string

		/*
		 * Restrict by email
		 */
		$this->filters['email'] = param( 'email', 'string', $this->default_filters['email'], true );

		/*
		 * Restrict by gender preset filter
		 */
		if( in_array( $this->filters['filter_preset'], array( 'men', 'women', 'other' ) ) )
		{
			switch( $this->filters['filter_preset'] )
			{
				case 'men':
					$this->filters['gender'] = 'M';
					break;
				case 'women':
					$this->filters['gender'] = 'F';
					break;
				case 'other':
					$this->filters['gender'] = 'O';
					break;
			}
		}

		/*
		 * Restrict by status
		 */
		$this->filters['account_status'] = param( 'account_status', 'string', $this->default_filters['account_status'], true );
		if( $this->filters['account_status'] === $this->default_filters['account_status'] &&
		    $this->filters['filter_preset'] == 'activated' )
		{
			$this->filters['status_activated'] = 1;
		}
		else
		{
			$this->filters['status_activated'] = $this->default_filters['status_activated'];
		}

		/*
		 * Restrict by registration date
		 */
		$this->filters['registered_min'] = param_date( 'registered_min', T_('Invalid date'), false, NULL );
		$this->filters['registered_max'] = param_date( 'registered_max', T_('Invalid date'), false, NULL );

		/*
		 * Restrict by reported state ( was reported or not )
		 */
		$this->filters['reported'] = param( 'reported', 'integer', $this->default_filters['reported'], true );

		/*
		 * Restrict by custom sender email settings
		 */
		$this->filters['custom_sender_email'] = param( 'custom_sender_email', 'integer', $this->default_filters['custom_sender_email'], true );
		$this->filters['custom_sender_name'] = param( 'custom_sender_name', 'integer', $this->default_filters['custom_sender_name'], true );

		/*
		 * Restrict by primary user group:
		 */
		$this->filters['group'] = param( 'group', 'integer', $this->default_filters['group'], true );

		/*
		 * Restrict by secondary user group:
		 */
		$this->filters['group2'] = param( 'group2', 'integer', $this->default_filters['group2'], true );

		/*
		 * Restrict by locations
		 */
		$this->filters['country'] = param( 'country', 'integer', $this->default_filters['country'], true );
		$this->filters['region'] = param( 'region', 'integer', $this->default_filters['region'], true );
		$this->filters['subregion'] = param( 'subregion', 'integer', $this->default_filters['subregion'], true );
		$this->filters['city'] = param( 'city', 'integer', $this->default_filters['city'], true );

		/*
		 * Restrict by age group
		 */
		$this->filters['age_min'] = param( 'age_min', 'integer', $this->default_filters['age_min'], true );
		$this->filters['age_max'] = param( 'age_max', 'integer', $this->default_filters['age_max'], true );

		/*
		 * Restrict by level
		 */
		$this->filters['level_min'] = param( 'level_min', 'integer', $this->default_filters['level_min'], true );
		$this->filters['level_max'] = param( 'level_max', 'integer', $this->default_filters['level_max'], true );

		/*
		 * Restrict by organization ID
		 */
		$this->filters['org'] = param( 'org', 'integer', $this->default_filters['org'], true );

		/*
		 * Restrict by newsletter ID
		 */
		$this->filters['newsletter'] = param( 'newsletter', 'integer', $this->default_filters['newsletter'], true );

		/*
		 * Restrict by not subscribed newsletter ID
		 */
		$this->filters['not_newsletter'] = param( 'not_newsletter', 'integer', $this->default_filters['not_newsletter'], true );

		/*
		 * Restrict by newsletter subscription activity
		 */
		$this->filters['newsletter_subscribed'] = param( 'newsletter_subscribed', 'integer', $this->default_filters['newsletter_subscribed'], true );

		/*
		 * Restrict by email campaign ID
		 */
		$this->filters['ecmp'] = param( 'ecmp', 'integer', $this->default_filters['ecmp'], true );

		/*
		 * Restrict by recipient type of email campaign
		 */
		$this->filters['recipient_type'] = param( 'recipient_type', 'string', $this->default_filters['recipient_type'], true );

		/*
		 * Restrict by recipient action on email campaign
		 */
		$this->filters['recipient_action'] = param( 'recipient_action', 'string', $this->default_filters['recipient_action'], true );

		/*
		 * Restrict by user tag
		 */
		$this->filters['user_tag'] = param( 'user_tag', 'string', $this->default_filters['user_tag'], true );

		/*
		 * Restrict by user tag
		 */
		$this->filters['not_user_tag'] = param( 'not_user_tag', 'string', $this->default_filters['user_tag'], true );


		// 'paged'
		$this->page = param( $this->page_param, 'integer', 1, true );      // List page number in paged display

		// 'order'
		global $Session;
		$prev_filters = $Session->get( $this->filterset_name );
		if( !empty( $prev_filters['order'] ) )
		{	// Restore an order from saved session
			$this->order = $this->filters['order'] = $prev_filters['order'];
		}

		if( $use_filters && $filter == 'new' )
		{
			$this->save_filterset();
		}

		return ! param_errors_detected();
	}


	/**
	 *
	 *
	 * @todo count?
	 */
	function query_init()
	{
		global $current_User;

		if( empty( $this->filters ) )
		{	// Filters have not been set before, we'll use the default filterset:
			// If there is a preset filter, we need to activate its specific defaults:
			$this->filters['filter_preset'] = param( $this->param_prefix.'filter_preset', 'string', $this->default_filters['filter_preset'], true );
			$this->activate_preset_filters();

			// Use the default filters:
			$this->set_filters( $this->default_filters );
		}

		// GENERATE THE QUERY:

		// The SQL Query object:
		// If group == -1 we shouldn't group list by user group
		$this->query_params['grouped'] = ( $this->filters['group'] != -1 );
		$this->UserQuery = new UserQuery( $this->Cache->dbtablename, $this->Cache->dbprefix, $this->Cache->dbIDname, $this->query_params );

		if( isset( $this->query_params['keywords_fields'] ) )
		{ // Change keywords_fields from query params
			$this->UserQuery->keywords_fields = $this->query_params['keywords_fields'];
		}

		if( isset( $this->query_params['where_status_closed'] ) )
		{ // Limit by closed users
			$this->UserQuery->where_status( 'closed', $this->query_params['where_status_closed'] );
		}

		// If browse users from different countries is restricted, then always filter to the current User country
		if( has_cross_country_restriction( 'users', 'list' ) )
		{ // Browse users from different countries is restricted, filter to current user country
			$ctry_filter = $current_User->ctry_ID;
			// if country filtering was changed the qurey must be refreshed
			$this->refresh_query = ( $this->refresh_query || ( $ctry_filter != $this->filters['country'] ) );
		}
		else
		{ // Browse users from different countries is allowed
			$ctry_filter = $this->filters['country'];
		}

		/*
		 * filtering stuff:
		 */
		$this->UserQuery->where_user_IDs( $this->filters['userids'] );
		$this->UserQuery->where_members( $this->filters['membersonly'] );
		$this->UserQuery->where_keywords( $this->filters['keywords'] );
		$this->UserQuery->where_email( $this->filters['email'] );
		$this->UserQuery->where_gender( $this->filters['gender'] );
		$this->UserQuery->where_status( $this->filters['status_activated'] );
		$this->UserQuery->where_status( $this->filters['account_status'], true, true );
		$this->UserQuery->where_registered_date( $this->filters['registered_min'], $this->filters['registered_max'] );
		$this->UserQuery->where_reported( $this->filters['reported'] );
		$this->UserQuery->where_custom_sender( $this->filters['custom_sender_email'], $this->filters['custom_sender_name'] );
		$this->UserQuery->where_group( $this->filters['group'] );
		$this->UserQuery->where_secondary_group( $this->filters['group2'] );
		$this->UserQuery->where_location( 'ctry', $ctry_filter );
		$this->UserQuery->where_location( 'rgn', $this->filters['region'] );
		$this->UserQuery->where_location( 'subrg', $this->filters['subregion'] );
		$this->UserQuery->where_location( 'city', $this->filters['city'] );
		$this->UserQuery->where_age_group( $this->filters['age_min'], $this->filters['age_max'] );
		$this->UserQuery->where_level( $this->filters['level_min'], $this->filters['level_max'] );
		if( isset( $this->filters['org'] ) || isset( $this->query_params['where_org_ID'] ) )
		{	// Filter by organization ID:
			$org_ID = isset( $this->query_params['where_org_ID'] ) ? $this->query_params['where_org_ID'] : $this->filters['org'];
			$this->UserQuery->where_organization( $org_ID );
		}
		$this->UserQuery->where_newsletter( $this->filters['newsletter'], $this->filters['newsletter_subscribed'] );
		$this->UserQuery->where_not_newsletter( $this->filters['not_newsletter'] );
		$this->UserQuery->where_email_campaign( $this->filters['ecmp'], $this->filters['recipient_type'], $this->filters['recipient_action'] );
		if( isset( $this->query_params['where_viewed_user'] ) )
		{	// Filter by user profile viewed:
			$this->UserQuery->where_viewed_user( $this->query_params['where_viewed_user'] );
		}
		if( isset( $this->filters['reg_ip_min'] ) && isset( $this->filters['reg_ip_max'] ) )
		{ // Filter by IP address:
			$this->UserQuery->where_reg_ip( $this->filters['reg_ip_min'], $this->filters['reg_ip_max'] );
		}
		if( ! is_logged_in() )
		{ // Restrict users by group level for anonymous users
			global $Settings;
			$this->UserQuery->where_group_level( $Settings->get('allow_anonymous_user_level_min'), $Settings->get('allow_anonymous_user_level_max') );
		}
		$this->UserQuery->where_tag( $this->filters['user_tag'], $this->filters['not_user_tag'] );
		$this->UserQuery->filter_query( $this->filters['filter_query'] );

		if( isset( $this->query_params['order_by_login_length'] ) && in_array( $this->query_params['order_by_login_length'], array( 'A', 'D' ) ) )
		{
			$this->UserQuery->ORDER_BY( 'CHAR_LENGTH(user_login) '.( $this->query_params['order_by_login_length'] == 'A' ? 'ASC' : 'DESC' ).', user_login ASC' );
		}
		elseif( $this->get_order_field_list() != '' )
		{
			$this->UserQuery->ORDER_BY( str_replace( '*', $this->get_order_field_list(), $this->UserQuery->order_by ) );
		}
		else
		{
			$this->UserQuery->ORDER_BY( $this->get_order_field_list() );
		}

		if( ! empty( $this->query_params['where_duplicate_email'] ) )
		{
			$this->UserQuery->where_duplicate_email();
		}
	}


	/**
	 * Run Query: GET DATA ROWS *** HEAVY ***
	 *
	 * We need this query() stub in order to call it from restart() and still
	 * let derivative classes override it
	 *
	 * @deprecated Use new function run_query()
	 */
	function query( $create_default_cols_if_needed = true, $append_limit = true, $append_order_by = true )
	{
		$this->run_query( $create_default_cols_if_needed, $append_limit, $append_order_by );
	}


	/**
	 * Run Query: GET DATA ROWS *** HEAVY ***
	 */
	function run_query( $create_default_cols_if_needed = true, $append_limit = true, $append_order_by = true,
											$query_title = 'Results::run_query()' )
	{
		global $DB, $Session, $localtimenow;

		if( !is_null( $this->rows ) )
		{ // Query has already executed:
			return;
		}

		// INIT THE QUERY:
		$this->query_init();


		// We are going to proceed in two steps (we simulate a subquery)
		// 1) we get the IDs we need
		// 2) we get all the other fields matching these IDs
		// This is more efficient than manipulating all fields at once.

		// *** STEP 1 ***
		$user_IDs = isset( $this->filters['users'] ) ? $this->filters['users'] : array();
		if( $this->refresh_query || // Some filters are changed
				$this->page == 1 || // Always run query on the first page
				$localtimenow - $Session->get( $this->filterset_name.'_refresh_time' ) > 7200 ) // Time has passed ( 2 hours )
		{	// We should create new list of user IDs
			global $Timer;
			$Timer->start( 'Users_IDs', false );

			$step1_SQL = new SQL( 'UserList::Query() Step 1: Get ID list' );
			$step1_SQL->SELECT( 'T_users.user_ID, IF( user_avatar_file_ID IS NOT NULL, 1, 0 ) as has_picture' );
			if( ! empty( $this->query_params['join_colls'] ) )
			{	// Initialize count of collections (used on order by this field):
				$step1_SQL->SELECT_add( ', COUNT( DISTINCT blog_ID ) AS nb_blogs' );
			}
			if( ! empty( $this->filters['reported'] ) && ( empty( $this->filters['filter_query'] ) || $this->check_filter_query( 'report_count', 0, '>' ) ) )
			{	// Filter is selected to 'Report count'
				$step1_SQL->SELECT_add( ', user_rep' );
			}
			if( ! empty( $this->query_params['join_sec_groups'] ) )
			{	// Initialize count of secondary groups (used on order by this field):
				$step1_SQL->SELECT_add( ', COUNT( DISTINCT sug_count.sug_grp_ID ) AS secondary_groups_count' );
			}
			if( ! empty( $this->query_params['where_duplicate_email'] ) )
			{
				$step1_SQL->SELECT_add( ', email_user_count' );
			}

			$step1_SQL->FROM( $this->UserQuery->get_from( '' ) );
			$step1_SQL->WHERE( $this->UserQuery->get_where( '' ) );
			$step1_SQL->WHERE_and( 'T_users.user_ID IS NOT NULL' );
			$step1_SQL->GROUP_BY( $this->UserQuery->get_group_by( '' ) );
			$step1_SQL->ORDER_BY( $this->UserQuery->get_order_by( '' ) );
			$step1_SQL->LIMIT( 0 );

			// Get list of the IDs we need:
			$user_IDs = $DB->get_col( $step1_SQL );
			// Update filter with user IDs
			$this->filters['users'] = $user_IDs;
			$this->save_filterset();

			$Timer->stop( 'Users_IDs' );
		}

		if( ! empty( $this->exclude_users ) )
		{	// Exclude users from filtered list:
			$exclude_users = explode( ',', $this->exclude_users );
			foreach( $exclude_users as $exclude_user_ID )
			{
				if( ( $exclude_user_ID_index = array_search( $exclude_user_ID, $user_IDs ) ) !== false )
				{	// This user should be excluded from filtered list:
					unset( $user_IDs[ $exclude_user_ID_index ] );
				}
			}
		}

		// GET TOTAL ROW COUNT:
		parent::count_total_rows( count( $user_IDs ) );

		// Pagination, Get user IDs from array for current page
		$user_IDs_paged = array_slice( $user_IDs, ( $this->page - 1 ) * $this->limit, $this->limit );

		// *** STEP 2 ***
		$step2_SQL = $this->UserQuery;

		if( ! empty( $user_IDs_paged ) )
		{	// Init sql query to get users by IDs
			$step2_SQL->WHERE( $this->Cache->dbIDname.' IN ('.implode( ',', $user_IDs_paged ).') ' );
			$step2_SQL->ORDER_BY( 'FIND_IN_SET( user_ID, "'.implode( ',', $user_IDs_paged ).'" )' );
		}
		else
		{	// No users
			$step2_SQL->WHERE( 'user_ID IS NULL' );
		}

		$this->sql = $step2_SQL->get();

		parent::run_query( false, false, false, 'UserList::Query() Step 2' );
	}


	/**
	 * Check if the Result set is filtered or not (compared to the preset)
	 */
	function is_filtered()
	{
		if( empty( $this->filters ) )
		{
			return false;
		}

		// Exclude user IDs
		// fp> WHY?
		unset( $this->filters['users'] );
		unset( $this->default_filters['users'] );

		// Exclude things that are npt actual filers:
		// unset( $this->filters['order'] );
		// unset( $this->default_filters['order'] );

		// pre_dump(  $this->filters, $this->default_filters );

		return ( $this->filters != $this->default_filters );
	}


	/**
	 * Link to previous and next link in collection
	 */
	function prevnext_user_links( $params )
	{
		$params = array_merge( array(
									'template'     => '$prev$$back$$next$',
									'prev_start'   => '',
									'prev_text'    => '&laquo; $login$',
									'prev_end'     => '',
									'prev_no_user' => '',
									'back_start'   => '',
									'back_end'     => '',
									'next_start'   => '',
									'next_text'    => '$login$ &raquo;',
									'next_end'     => '',
									'next_no_user' => '',
									'user_tab'     => 'profile',
								), $params );

		// ID of selected user
		$user_ID = get_param( 'user_ID' );

		$users_list = $this->filters['users'];
		if( array_search( $user_ID, $users_list ) === false )
		{	// Selected user is NOT located in this list
			return;
		}

		$prev = $this->prevnext_user_link( 'prev', $params['prev_start'], $params['prev_end'], $params['prev_text'], $params['prev_no_user'], $params['user_tab'], false );
		$next = $this->prevnext_user_link( 'next', $params['next_start'], $params['next_end'], $params['next_text'], $params['next_no_user'], $params['user_tab'], false );
		$back = $this->back_user_link( $params['back_start'], $params['back_end'], false );

		$output = str_replace( '$prev$', $prev, $params['template'] );
		$output = str_replace( '$next$', $next, $output );
		$output = str_replace( '$back$', $back, $output );

		if( !empty( $output ) )
		{	// we have some output, lets wrap it
			echo( $params['block_start'] );
			echo $output;
			echo( $params['block_end'] );
		}
	}


	/**
	 * Get link to previous/next user
	 *
	 * @return string Link to previous/next user
	 */
	function prevnext_user_link( $direction, $before = '', $after = '', $text = '&laquo; $login$', $no_user = '', $user_tab = 'profile', $display = true )
	{
		/**
		 * @var User
		 */
		$prev_User = & $this->get_prevnext_User( $direction );
		if( has_cross_country_restriction( 'users', 'list' ) )
		{ // If current user has cross country restriction, make sure we display only users from the same country
			// Note: This may happen only if the user list filter was saved and the cross country restriction was changed after that
			global $current_User;
			while( !empty( $prev_User ) && ( $current_User->ctry_ID !== $prev_User->ctry_ID ) )
			{
				$prev_User = & $this->get_prevnext_User( $direction, $prev_User->ID );
			}
		}

		if( !empty( $prev_User ) )
		{	// User exists in DB
			$output = $before;
			$identity_url = get_user_identity_url( $prev_User->ID, $user_tab );
			$login = str_replace( '$login$', $prev_User->get_colored_login( array( 'login_text' => 'name' ) ), $text );
			if( !empty( $identity_url ) )
			{	// User link is available
				// Note: we don't want a bubble tip on navigation links
				$output .= '<a href="'.$identity_url.'">'.$login.'</a>';
			}
			else
			{	// No identity link
				$output .= $login;
			}
			$output .= $after;
		}
		else
		{	// No user
			$output = $no_user;
		}
		if( $display ) echo $output;
		return $output;
	}


	/**
	 * Get link to back users list
	 *
	 * @return string Link to back users list
	 */
	function back_user_link( $before = '', $after = '', $display = true )
	{
		// ID of selected user
		$user_ID = get_param( 'user_ID' );

		$users_list = $this->filters['users'];

		$user_key = array_search( $user_ID, $users_list );
		if( is_int( $user_key ) )
		{	// Selected user is located in this list
			global $Collection, $Blog;
			++$user_key;
			$page = ceil( $user_key / $this->limit );
			$page_param = '';
			if( $page > 1 )
			{
				$page_param = $this->page_param.'='.$page;
			}

			$output = $before;
			$output .= '<a href="'.get_dispctrl_url( 'users', $page_param ).'">'.$user_key.'/'.count( $users_list ).'</a>';
			$output .= $after;
		}
		else
		{
			$output = '';
		}
		if( $display ) echo $output;
		return $output;
	}


	/**
	 * Skip to previous/next User
	 *
	 * @param integer the currently selected user ID ( Note: it must be set only if we would like to skip some users from the list )
	 * @param string prev | next  (relative to the current sort order)
	 */
	function & get_prevnext_User( $direction = 'next', $selected_user_ID = NULL )
	{
		$users_list = $this->filters['users'];

		if( count( $users_list ) < 2 )
		{	// Short users list
			$r = NULL;
			return $r;
		}

		// ID of selected user
		if( $selected_user_ID === NULL )
		{ // get currently selected user ID from param
			$selected_user_ID = get_param( 'user_ID' );
		}

		$user_key = array_search( $selected_user_ID, $users_list );
		if( is_int( $user_key ) )
		{	// Selected user is located in the list
			$prevnext_key = $direction == 'next' ? $user_key + 1 : $user_key - 1;
			if( isset( $users_list[$prevnext_key] ) )
			{	// Prev/next user is located in the list
				$prevnext_ID = $users_list[$prevnext_key];
			}
		}

		if( empty( $prevnext_ID ) )
		{	// No prev/next user
			$r = NULL;
			return $r;
		}

		$UserCache = & get_UserCache();
		$User = & $UserCache->get_by_ID( $prevnext_ID, false, false );

		return $User;
	}


	/**
	 * Set an order of a list (Use this function after when all $this->cols are already defined)
	 *
	 * @param string Field name
	 * @param string Order direction (A|D)
	 * @param boolean Save the filters from Session
	 */
	function set_order( $order_field, $direction = 'D', $save_filters = false )
	{
		global $Session;

		if( empty( $this->cols ) )
		{ // The columns are not defined yet, Exit here
			return;
		}

		// Force filter param to reset the previous filters
		set_param( 'filter', 'new' );
		$this->refresh_query = true;

		foreach( $this->cols as $col_num => $col )
		{	// Find a column number
			if( $col['order'] == $order_field )
			{
				break;
			}
		}

		if( $save_filters )
		{ // Get the filters from Session
			$this->filters = $Session->get( $this->filterset_name );
			if( ! is_array( $this->filters ) )
			{
				$this->filters = array();
			}
			$this->filters = array_merge( $this->default_filters, $this->filters );
		}
		else
		{	// Reset the filters:
			$this->filters = $this->default_filters;
			// We want to reset the memorized filterset:
			$Session->delete( $this->filterset_name );
			// Memorize global variables:
			$this->set_filters( array() );
		}

		// Rewrite a previous order to new value
		$this->filters['order'] = str_repeat( '-', $col_num ).$direction;
		$this->order = $this->filters['order'];

		// Save a new order
		$Session->set( $this->filterset_name, $this->filters );
		$this->save_filterset();
	}

}

?>