<?php /*

 ocPortal
 Copyright (c) ocProducts, 2004-2012

 See text/EN/licence.txt for full licencing information.


 NOTE TO PROGRAMMERS:
   Do not edit this file. If you need to make changes, save your changed file to the appropriate *_custom folder
   **** If you ignore this advice, then your website upgrades (e.g. for bug fixes) will likely kill your changes ****

*/

/**
 * @license		http://opensource.org/licenses/cpal_1.0 Common Public Attribution License
 * @copyright	ocProducts Ltd
 * @package		core_ocf
 */

/**
 * Standard code module initialisation function.
 */
function init__forum__ocf()
{
	global $LDAP_CONNECTION;
	$LDAP_CONNECTION=NULL;
	global $EMOTICON_LEVELS;
	$EMOTICON_LEVELS=NULL;
	global $FLOOD_CONTROL_ONCE;
	$FLOOD_CONTROL_ONCE=false;
	global $SENT_OUT_VALIDATE_NOTICE;
	$SENT_OUT_VALIDATE_NOTICE=false;
	global $LAST_POST_ID,$LAST_TOPIC_ID;
	$LAST_POST_ID=NULL;
	$LAST_TOPIC_ID=NULL;
	global $TOPIC_IDENTIFIERS_TO_IDS,$FORUM_NAMES_TO_IDS,$TOPIC_IS_THREADED;
	$TOPIC_IDENTIFIERS_TO_IDS=array();
	$FORUM_NAMES_TO_IDS=array();
	$TOPIC_IS_THREADED=array();
}

class forum_driver_ocf extends forum_driver_base
{

	/**
	 * Initialise LDAP. To see if LDAP is running we check LDAP_CONNECTION for NULL. ldap_is_enabled is not good enough - we don't want ocPortal to bomb out under faulty LDAP settings, hence making it unfixable.
	 */
	function forum_layer_initialise()
	{
		$GLOBALS['OCF_DRIVER']=&$this;

		if ((function_exists('ldap_connect')) && (get_option('ldap_is_enabled',true)=='1'))
		{
			require_code('ocf_members');
			require_code('ocf_groups');
			require_code('ocf_ldap');

			ocf_ldap_connect();
		}
	}

	/**
	 * Get the rows for the top given number of posters on the forum.
	 *
	 * @param  integer		The limit to the number of top posters to fetch
	 * @return array			The rows for the given number of top posters in the forum
	 */
	function get_top_posters($limit)
	{
		return $this->connection->query('SELECT * FROM '.$this->connection->get_table_prefix().'f_members WHERE id<>'.strval((integer)$this->get_guest_id()).' ORDER BY m_cache_num_posts DESC',$limit);
	}

	/**
	 * Get the forums' table prefix for the database.
	 *
	 * @return string			The forum database table prefix
	 */
	function get_drivered_table_prefix()
	{
		global $SITE_INFO;
		return array_key_exists('ocf_table_prefix',$SITE_INFO)?$SITE_INFO['ocf_table_prefix']:get_table_prefix();
	}

	/**
	 * Attempt to to find the member's language from their forum profile. It converts between language-identifiers using a map (lang/map.ini).
	 *
	 * @param  MEMBER				The member who's language needs to be fetched
	 * @return ?LANGUAGE_NAME	The member's language (NULL: unknown)
	 */
	function forum_get_lang($member)
	{
		return $this->get_member_row_field($member,'m_language');
	}
	
	/**
	 * Find if login cookie is md5-hashed.
	 *
	 * @return boolean		Whether the login cookie is md5-hashed
	 */
	function is_hashed()
	{
		return true;
	}
	
	/**
	 * Find if the login cookie contains the login name instead of the member id.
	 *
	 * @return boolean		Whether the login cookie contains a login name or a member id
	 */
	function is_cookie_login_name()
	{
		return false;
	}
	
	/**
	 * Find the member id of the forum guest member.
	 *
	 * @return MEMBER			The member id of the forum guest member
	 */
	function get_guest_id()
	{
		static $ret=NULL;
		if ($ret===NULL) $ret=db_get_first_id();
		return $ret;
	}
	
	/**
	 * Add the specified custom field to the forum (some forums implemented this using proper custom profile fields, others through adding a new field).
	 *
	 * @param  string			The name of the new custom field
	 * @param  integer		The length of the new custom field
	 * @param  BINARY			Whether the field is locked
	 * @param  BINARY			Whether the field is for viewing
	 * @param  BINARY			Whether the field is for setting
	 * @param  BINARY			Whether the field is required
	 * @param  string			Description
	 * @param  string			The field type
	 * @param  BINARY			Whether the field is encrypted
	 * @param  ?string		Default field value (NULL: standard for field type)
	 * @return boolean		Whether the custom field was created successfully
	 */
	function install_create_custom_field($name,$length,$locked=1,$viewable=0,$settable=0,$required=0,$description='',$type='long_text',$encrypted=0,$default=NULL)
	{
		require_code('ocf_forum_driver_helper_install');
		return _helper_install_create_custom_field($this,$name,$length,$locked,$viewable,$settable,$required,$description,$type,$encrypted,$default);
	}
	
	/**
	 * Get an array of attributes to take in from the installer. Almost all forums require a table prefix, which the requirement there-of is defined through this function.
	 * The attributes have 4 values in an array
	 * - name, the name of the attribute for info.php
	 * - default, the default value (perhaps obtained through autodetection from forum config)
	 * - description, a textual description of the attributes
	 * - title, a textual title of the attribute
	 *
	 * @return array			The attributes for the forum
	 */
	function install_specifics()
	{
		require_code('ocf_forum_driver_helper_install');
		return _helper_install_specifics();
	}
	
	/**
	 * Searches for forum auto-config at this path.
	 *
	 * @param  PATH			The path in which to search
	 * @return boolean		Whether the forum auto-config could be found
	 */
	function install_test_load_from($path)
	{
		require_code('ocf_forum_driver_helper_install');
		return _helper_install_test_load_from($path);
	}
	
	/**
	 * Get an array of paths to search for config at.
	 *
	 * @return array			The paths in which to search for the forum config
	 */
	function install_get_path_search_list()
	{
		return array(get_file_base());
	}
	
	/**
	 * Makes a post in the specified forum, in the specified topic according to the given specifications. If the topic doesn't exist, it is created along with a spacer-post.
	 * Spacer posts exist in order to allow staff to delete the first true post in a topic. Without spacers, this would not be possible with most forum systems. They also serve to provide meta information on the topic that cannot be encoded in the title (such as a link to the content being commented upon).
	 *
	 * @param  SHORT_TEXT	The forum name
	 * @param  SHORT_TEXT	The topic identifier (usually <content-type>_<content-id>)
	 * @param  MEMBER			The member ID
	 * @param  LONG_TEXT		The post title
	 * @param  LONG_TEXT		The post content in Comcode format
	 * @param  string			The topic title; must be same as content title if this is for a comment topic
	 * @param  string			This is put together with the topic identifier to make a more-human-readable topic title or topic description (hopefully the latter and a $content_title title, but only if the forum supports descriptions)
	 * @param  ?URLPATH		URL to the content (NULL: do not make spacer post)
	 * @param  ?TIME			The post time (NULL: use current time)
	 * @param  ?IP				The post IP address (NULL: use current members IP address)
	 * @param  ?BINARY		Whether the post is validated (NULL: unknown, find whether it needs to be marked unvalidated initially). This only works with the OCF driver.
	 * @param  ?BINARY		Whether the topic is validated (NULL: unknown, find whether it needs to be marked unvalidated initially). This only works with the OCF driver.
	 * @param  boolean		Whether to skip post checks
	 * @param  SHORT_TEXT	The name of the poster
	 * @param  ?AUTO_LINK	ID of post being replied to (NULL: N/A)
	 * @param  boolean		Whether the reply is only visible to staff
	 * @return array			Topic ID (may be NULL), and whether a hidden post has been made
	 */
	function make_post_forum_topic($forum_name,$topic_identifier,$member_id,$post_title,$post,$content_title,$topic_identifier_encapsulation_prefix,$content_url=NULL,$time=NULL,$ip=NULL,$validated=NULL,$topic_validated=1,$skip_post_checks=false,$poster_name_if_guest='',$parent_id=NULL,$staff_only=false)
	{
		require_code('ocf_forum_driver_helper');
		return _helper_make_post_forum_topic($this,$forum_name,$topic_identifier,$member_id,$post_title,$post,$content_title,$topic_identifier_encapsulation_prefix,$content_url,$time,$ip,$validated,$topic_validated,$skip_post_checks,$poster_name_if_guest,$parent_id,$staff_only);
	}

	/**
	 * Get an array of topics in the given forum. Each topic is an array with the following attributes:
	 * - id, the topic ID
	 * - title, the topic title
	 * - lastusername, the username of the last poster
	 * - lasttime, the timestamp of the last reply
	 * - closed, a Boolean for whether the topic is currently closed or not
	 * - firsttitle, the title of the first post
	 * - firstpost, the first post (only set if $show_first_posts was true)
	 *
	 * @param  mixed			The forum name or an array of forum IDs (in such an array the KEYS are forum IDs, and the values ignored)
	 * @param  integer		The limit
	 * @param  integer		The start position
	 * @param  integer		The total rows (not a parameter: returns by reference)
	 * @param  SHORT_TEXT	The topic title filter
	 * @param  boolean		Whether to show the first posts
	 * @param  string			The date key to sort by
	 * @set    lasttime firsttime
	 * @param  boolean		Whether to limit to hot topics
	 * @param  SHORT_TEXT	The topic description filter
	 * @return ?array			The array of topics (NULL: error/none)
	 */
	function show_forum_topics($name,$limit,$start,&$max_rows,$filter_topic_title='',$show_first_posts=false,$date_key='lasttime',$hot=false,$filter_topic_description='')
	{
		require_code('ocf_forum_driver_helper');
		return _helper_show_forum_topics($this,$name,$limit,$start,$max_rows,$filter_topic_title,$filter_topic_description,$show_first_posts,$date_key,$hot);
	}

	/**
	 * Get an array of maps for the topic in the given forum.
	 *
	 * @param  integer		The topic ID
	 * @param  integer		The comment count will be returned here by reference
	 * @param  integer		Maximum comments to returned
	 * @param  integer		Comment to start at
	 * @param  boolean		Whether to mark the topic read
	 * @param  boolean		Whether to show in reverse
	 * @param  boolean		Whether to only load minimal details if it is a threaded topic
	 * @param  ?array			List of post IDs to load (NULL: no filter)
	 * @param  boolean		Whether to load spacer posts
	 * @return mixed			The array of maps (Each map is: title, message, member, date) (-1 for no such forum, -2 for no such topic)
	 */
	function get_forum_topic_posts($topic_id,&$count,$max=100,$start=0,$mark_read=true,$reverse=false,$light_if_threaded=false,$posts=NULL,$load_spacer_posts_too=false)
	{
		require_code('ocf_forum_driver_helper');
		return _helper_get_forum_topic_posts($this,$topic_id,$count,$max,$start,$mark_read,$reverse,$light_if_threaded,$posts,$load_spacer_posts_too);
	}

	/**
	 * Load extra details for a list of posts. Does not need to return anything if forum driver doesn't support partial post loading (which is only useful for threaded topic partial-display).
	 *
	 * @param  AUTO_LINK		Topic the posts come from
	 * @param  array			List of post IDs
	 * @return array			Extra details
	 */
	function get_post_remaining_details($topic_id,$post_ids)
	{
		require_code('ocf_forum_driver_helper');
		return _helper_get_post_remaining_details($this,$topic_id,$post_ids);
	}

	/**
	 * Find whether a forum is threaded.
	 *
	 * @param  integer		The topic ID
	 * @return boolean		Whether it is
	 */
	function topic_is_threaded($topic_id)
	{
		global $TOPIC_IS_THREADED;
		if (array_key_exists($topic_id,$TOPIC_IS_THREADED)) return $TOPIC_IS_THREADED[$topic_id]==1;

		$TOPIC_IS_THREADED[$topic_id]=$this->connection->query_value_null_ok('f_topics t JOIN '.$this->connection->get_table_prefix().'f_forums f ON f.id=t.t_forum_id','f_is_threaded',array('t.id'=>$topic_id));
		return $TOPIC_IS_THREADED[$topic_id]==1;
	}

	/**
	 * Get an emoticon chooser template.
	 *
	 * @param  string			The ID of the form field the emoticon chooser adds to
	 * @return tempcode		The emoticon chooser template
	 */
	function get_emoticon_chooser($field_name='post')
	{
		require_code('ocf_forum_driver_helper');
		return _helper_get_emoticon_chooser($this,$field_name);
	}

	/**
	 * Pin a topic.
	 *
	 * @param  AUTO_LINK		The topic ID
	 */
	function pin_topic($id)
	{
		$this->connection->query_update('f_topics',array('t_pinned'=>1),array('id'=>$id),'',1);
	}

	/**
	 * Find the base URL to the emoticons.
	 *
	 * @return URLPATH		The base URL
	 */
	function get_emo_dir()
	{
		return '';
	}
	
	/**
	 * Get a map between smiley codes and templates representing the HTML-image-code for this smiley. The smilies present of course depend on the forum involved.
	 *
	 * @param  ?MEMBER		Only emoticons the given member can see (NULL: don't care)
	 * @return array			The map
	 */
	function find_emoticons($member=NULL)
	{
		require_code('ocf_forum_driver_helper');
		return _helper_apply_emoticons($this,$member);
	}
	
	/**
	 * Try to find the theme that the logged-in/guest member is using, and map it to an ocPortal theme.
	 * The themes/map.ini file functions to provide this mapping between forum themes, and ocPortal themes, and has a slightly different meaning for different forum drivers. For example, some drivers map the forum themes theme directory to the ocPortal theme name, whilst others made the humanly readeable name.
	 *
	 * @param  boolean		Whether to avoid member-specific lookup
	 * @return ID_TEXT		The theme
	 */
	function _get_theme($skip_member_specific=false)
	{
		$member=get_member();
		$theme='';
		if (!$skip_member_specific)
		{
			if ($member!=$this->get_guest_id())
			{
				$theme=$this->get_member_row_field($member,'m_theme');
			}
		}
		if (($theme=='') || ($theme=='-1'))
		{
			$theme=substr(preg_replace('#[^A-Za-z\d]#','_',get_site_name()),0,80);
			if (is_dir(get_custom_file_base().'/themes/'.$theme)) return $theme;
			$theme='default';
		}
		return $theme;
	}
	
	/**
	 * Set a custom profile field's value. It should not be called directly.
	 *
	 * @param  MEMBER			The member id
	 * @param  string			The field name
	 * @param  string			The value
	 */
	function set_custom_field($member,$field,$amount)
	{
		// Check member exists
		$username=$this->get_username($member);
		if (is_null($username)) return;

		require_code('ocf_members_action');
		require_code('ocf_members_action2');

		$field_bits=$this->connection->query_select('f_custom_fields f LEFT JOIN '.$this->connection->get_table_prefix().'translate t ON t.id=f.cf_name',array('f.id','f.cf_type'),array('text_original'=>'ocp_'.$field));
		if (!array_key_exists(0,$field_bits)) // Should never happen, but sometimes on upgrades/corruption...
		{
			$this->install_create_custom_field($field,10);
			$field_bits=$this->connection->query_select('f_custom_fields f LEFT JOIN '.$this->connection->get_table_prefix().'translate t ON t.id=f.cf_name',array('f.id','f.cf_type'),array('text_original'=>'ocp_'.$field));
			if (!array_key_exists(0,$field_bits)) return; // Possible on an MSN, and there's an inconsistency (e.g. no points addon)
		}
		$field_type=$field_bits[0]['cf_type'];
		$field_id=$field_bits[0]['id'];
		if ($field_type=='integer')
		{
			ocf_set_custom_field($member,$field_id,intval($amount));
		} else
		{
			ocf_set_custom_field($member,$field_id,$amount);
		}
	}

	/**
	 * Get custom profile fields values for all 'ocp_' prefixed keys.
	 *
	 * @param  MEMBER			The member id
	 * @return ?array			A map of the custom profile fields, key_suffix=>value (NULL: no fields)
	 */
	function get_custom_fields($member)
	{
		// Check member exists
		$username=$this->get_username($member);
		if (is_null($username)) return;

		require_code('ocf_members');

		$info=ocf_get_all_custom_fields_match_member($member,NULL,NULL,NULL,NULL,NULL,NULL,NULL,1);
		$out=array();
		foreach ($info as $field=>$value)
		{
			$out[substr($field,4)]=$value['RAW'];
		}
		return $out;
	}
	
	/**
	 * Get a member profile-row for the member of the given name.
	 *
	 * @param  SHORT_TEXT	The member name
	 * @return ?array			The profile-row (NULL: could not find)
	 */
	function pget_row($name)
	{
		foreach ($this->MEMBER_ROWS_CACHED as $i=>$row)
		{
			if ($row['m_username']==$name) return $row;
		}
		$rows=$this->connection->query_select('f_members',array('*'),array('m_username'=>$name),'',1);
		if (!array_key_exists(0,$rows)) return NULL;
		return $rows[0];
	}
	
	/**
	 * From a member profile-row, get the member's primary usergroup.
	 *
	 * @param  array			The profile-row
	 * @return GROUP			The member's primary usergroup
	 */
	function pname_group($r)
	{
		require_code('ocf_members');
		return ocf_get_member_primary_group($r['id']);
	}
	
	/**
	 * From a member profile-row, get the member's member id.
	 *
	 * @param  array			The profile-row
	 * @return MEMBER			The member id
	 */
	function pname_id($r)
	{
		return $r['id'];
	}
	
	/**
	 * From a member profile-row, get the member's last visit date.
	 *
	 * @param  array			The profile-row
	 * @return TIME			The last visit date
	 */
	function pnamelast_visit($r)
	{
		return $r['m_last_visit_time'];
	}

	/**
	 * From a member profile-row, get the member's name.
	 *
	 * @param  array			The profile-row
	 * @return string			The member name
	 */
	function pname_name($r)
	{
		return $r['m_username'];
	}
	
	/**
	 * From a member profile-row, get the member's e-mail address.
	 *
	 * @param  array			The profile-row
	 * @return SHORT_TEXT	The member e-mail address
	 */
	function pname_email($r)
	{
		return $r['m_email_address'];
	}

	/**
	 * Get a URL to the specified member's home (control panel).
	 *
	 * @param  MEMBER			The member id
	 * @return URLPATH		The URL to the members home
	 */
	function member_home_url($id)
	{
		$_url=build_url(array('page'=>'members','type'=>'view','id'=>$id),get_module_zone('members'),NULL,false,false,false,'tab__edit');
		$url=$_url->evaluate();
		if (get_option('forum_in_portal')=='0') $url=str_replace(get_base_url(),get_forum_base_url(),$url);
		return $url;
	}

	/**
	 * Get a URL to the specified member's profile.
	 *
	 * @param  MEMBER			The member id
	 * @param  boolean		Whether it is okay to return the result using Tempcode (more efficient, and allows keep_* parameters to propagate which you almost certainly want!)
	 * @return mixed			The URL to the member profile
	 */
	function _member_profile_url($id,$tempcode_okay=false)
	{
		if (get_value('username_profile_links')=='1')
		{
			$username=$GLOBALS['FORUM_DRIVER']->get_username($id);
			$map=array('page'=>'members','type'=>'view','id'=>is_null($username)?strval($id):$username);
			if (get_page_name()=='members') $map+=propagate_ocselect();
			$_url=build_url($map,get_module_zone('members'),NULL,false,false,!$tempcode_okay);
		} else
		{
			$map=array('page'=>'members','type'=>'view','id'=>$id);
			if (get_page_name()=='members') $map+=propagate_ocselect();
			$_url=build_url($map,get_module_zone('members'),NULL,false,false,!$tempcode_okay);
		}
		if (($tempcode_okay) && (get_base_url()==get_forum_base_url())) return $_url;
		$url=$_url->evaluate();
		if (get_option('forum_in_portal')=='0') $url=str_replace(get_base_url(),get_forum_base_url(),$url);
		return $url;
	}

	/**
	 * Get a URL to the specified member's profile, from the username.
	 *
	 * @param  SHORT_TEXT	The username
	 * @return URLPATH		The URL to the member profile
	 */
	function member_profile_url_name($name)
	{
		$_url=build_url(array('page'=>'members','type'=>'view','id'=>$this->get_member_from_username($name)));
		$url=$_url->evaluate();
		if (get_option('forum_in_portal')=='0') $url=str_replace(get_base_url(),get_forum_base_url(),$url);
		return $url;
	}

	/**
	 * Get a URL to the registration page (for people to create member accounts).
	 *
	 * @return URLPATH		The URL to the registration page
	 */
	function _join_url()
	{
		$page='_SELF';
		if (count($_POST)!=0) $page='';
		$_redirect_url=build_url(array('page'=>$page),'_SELF',array('keep_session'=>1,'redirect'=>1),true);
		$redirect_url=$_redirect_url->evaluate();

		$redirect_url=get_param('redirect_passon',get_param('redirect',$redirect_url));

		$_url=build_url(array('page'=>'join','redirect'=>$redirect_url),get_module_zone('join'));
		$url=$_url->evaluate();
		if (get_option('forum_in_portal')=='0') $url=str_replace(get_base_url(),get_forum_base_url(),$url);
		return $url;
	}

	/**
	 * Get a URL to the members-online page.
	 *
	 * @return URLPATH		The URL to the members-online page
	 */
	function _online_members_url()
	{
		$_url=build_url(array('page'=>'onlinemembers'),get_module_zone('onlinemembers'));
		$url=$_url->evaluate();
		if (get_option('forum_in_portal')=='0') $url=str_replace(get_base_url(),get_forum_base_url(),$url);
		return $url;
	}

	/**
	 * Get a URL to send a private/personal message to the given member.
	 *
	 * @param  MEMBER			The member id
	 * @return URLPATH		The URL to the private/personal message page
	 */
	function _member_pm_url($id)
	{
		$_url=build_url(array('page'=>'topics','type'=>'new_pt','id'=>$id),get_module_zone('topics'));
		$url=$_url->evaluate();
		if (get_option('forum_in_portal')=='0') $url=str_replace(get_base_url(),get_forum_base_url(),$url);
		return $url;
	}

	/**
	 * Get a URL to the specified forum.
	 *
	 * @param  integer		The forum ID
	 * @param  boolean		Whether it is okay to return the result using Tempcode (more efficient)
	 * @return mixed			The URL to the specified forum
	 */
	function _forum_url($id,$tempcode_okay=false)
	{
		$view_map=array('page'=>'forumview');
		if ($id!=db_get_first_id()) $view_map['id']=$id;
		$_url=build_url($view_map,get_module_zone('forumview'),NULL,false,false,!$tempcode_okay);
		if (($tempcode_okay) && (get_base_url()==get_forum_base_url())) return $_url;
		$url=$_url->evaluate();
		if (get_option('forum_in_portal')=='0') $url=str_replace(get_base_url(),get_forum_base_url(),$url);
		return $url;
	}

	/**
	 * Get the forum ID from a forum name.
	 *
	 * @param  SHORT_TEXT	The forum name
	 * @return integer		The forum ID
	 */
	function forum_id_from_name($forum_name)
	{
		global $FORUM_NAMES_TO_IDS;
		if (array_key_exists($forum_name,$FORUM_NAMES_TO_IDS)) return $FORUM_NAMES_TO_IDS[$forum_name];

		if (is_numeric($forum_name))
		{
			$result=intval($forum_name);
		} else
		{
			$_result=$this->connection->query_select('f_forums',array('id','f_is_threaded'),array('f_name'=>$forum_name),'',1);
			$result=mixed();
			if (array_key_exists(0,$_result))
			{
				$result=$_result[0]['id'];
			}
		}

		$FORUM_NAMES_TO_IDS[$forum_name]=$result;
		return $result;
	}

	/**
	 * Get the topic ID from a topic identifier in the specified forum. It is used by comment topics, which means that the unique-topic-name assumption holds valid.
	 *
	 * @param  string			The forum name / ID
	 * @param  SHORT_TEXT	The topic identifier
	 * @return integer		The topic ID
	 */
	function find_topic_id_for_topic_identifier($forum,$topic_identifier)
	{
		$key=serialize(array($forum,$topic_identifier));

		global $TOPIC_IDENTIFIERS_TO_IDS;
		if (array_key_exists($key,$TOPIC_IDENTIFIERS_TO_IDS)) return $TOPIC_IDENTIFIERS_TO_IDS[$key];
		$result=is_numeric($forum)?intval($forum):$this->connection->query_value_null_ok('f_forums','id',array('f_name'=>$forum));

		if (is_integer($forum)) $forum_id=$forum;
		else $forum_id=$this->forum_id_from_name($forum);
		$query='SELECT t.id,f_is_threaded FROM '.$this->connection->get_table_prefix().'f_topics t JOIN '.$this->connection->get_table_prefix().'f_forums f ON f.id=t.t_forum_id WHERE t_forum_id='.strval((integer)$forum_id).' AND ('.db_string_equal_to('t_description',$topic_identifier).' OR t_description LIKE \'%: #'.db_encode_like($topic_identifier).'\')';

		$_result=$this->connection->query($query,1);
		if (array_key_exists(0,$_result))
		{
			$TOPIC_IDENTIFIERS_TO_IDS[$key]=$_result[0]['id'];
			global $TOPIC_IS_THREADED;
			$TOPIC_IS_THREADED[$_result[0]['id']]=$_result[0]['f_is_threaded'];
		} else
		{
			$TOPIC_IDENTIFIERS_TO_IDS[$key]=NULL;
		}
		return $TOPIC_IDENTIFIERS_TO_IDS[$key];
	}

	/**
	 * Get a URL to the specified topic ID. Most forums don't require the second parameter, but some do, so it is required in the interface.
	 *
	 * @param  integer		The topic ID
	 * @param  string			The forum ID
	 * @param  boolean		Whether it is okay to return the result using Tempcode (more efficient)
	 * @return mixed			The URL to the topic
	 */
	function topic_url($id,$forum='',$tempcode_okay=false)
	{
		if (is_null($id)) return ''; // Should not happen, but if it does, this is how we should handle it.

		unset($forum);
		$_url=build_url(array('page'=>'topicview','id'=>$id),get_module_zone('topicview'),NULL,false,false,!$tempcode_okay);
		if (($tempcode_okay) && (get_base_url()==get_forum_base_url())) return $_url;
		$url=$_url->evaluate();
		if (get_option('forum_in_portal')=='0') $url=str_replace(get_base_url(),get_forum_base_url(),$url);
		return $url;
	}

	/**
	 * Get a URL to the specified post id.
	 *
	 * @param  integer		The post id
	 * @param  string			The forum ID
	 * @param  boolean		Whether it is okay to return the result using Tempcode (more efficient)
	 * @return mixed			The URL to the post
	 */
	function post_url($id,$forum,$tempcode_okay=false)
	{
		if (is_null($id)) return ''; // Should not happen, but if it does, this is how we should handle it.

		unset($forum);

		$_url=build_url(array('page'=>'topicview','type'=>'findpost','id'=>$id),get_module_zone('topicview'),NULL,false,false,!$tempcode_okay);
		if (($tempcode_okay) && (get_base_url()==get_forum_base_url())) return $_url;
		$url=$_url->evaluate();
		$url.='#post_'.strval($id);
		if (get_option('forum_in_portal')=='0') $url=str_replace(get_base_url(),get_forum_base_url(),$url);
		return $url;
	}

	/**
	 * Get an array of members who are in at least one of the given array of usergroups.
	 *
	 * @param  array			The array of usergroups
	 * @param  ?integer		Return up to this many entries for primary members and this many entries for secondary members (NULL: no limit, only use no limit if querying very restricted usergroups!)
	 * @param  integer		Return primary members after this offset and secondary members after this offset
	 * @return ?array			The map of members, member ID to details (NULL: no members)
	 */
	function member_group_query($groups,$max=NULL,$start=0)
	{
		$_groups='';
		foreach ($groups as $group)
		{
			if ($_groups!='') $_groups.=' OR ';
			$_groups.='gm_group_id='.strval((integer)$group);
		}
		if ($_groups=='') return array();
		$a=$this->connection->query('SELECT u.* FROM '.$this->connection->get_table_prefix().'f_group_members g LEFT JOIN '.$this->connection->get_table_prefix().'f_members u ON u.id=g.gm_member_id WHERE ('.$_groups.') AND gm_validated=1 ORDER BY g.gm_group_id ASC',$max,$start);
		$_groups='';
		foreach ($groups as $group)
		{
			if ($_groups!='') $_groups.=' OR ';
			$_groups.='m_primary_group='.strval((integer)$group);
		}
		$b=$this->connection->query('SELECT * FROM '.$this->connection->get_table_prefix().'f_members WHERE '.$_groups.' ORDER BY m_primary_group ASC',$max,$start);
		$out=array();
		foreach ($a as $x)
			if (!array_key_exists($x['id'],$out)) $out[$x['id']]=$x;
		foreach ($b as $x)
			if (!array_key_exists($x['id'],$out)) $out[$x['id']]=$x;

		// Now implicit usergroup hooks
		$hooks=find_all_hooks('systems','ocf_implicit_usergroups');
		foreach (array_keys($hooks) as $hook)
		{
			require_code('hooks/systems/ocf_implicit_usergroups/'.$hook);
			$ob=object_factory('Hook_implicit_usergroups_'.$hook);
			if (in_array($ob->get_bound_group_id(),$groups))
			{
				$c=$ob->get_member_list();
				if (!is_null($c))
				{
					foreach ($c as $member_id=>$x)
						$out[$member_id]=$x;
				}
			}
		}

		return $out;
	}
	
	/**
	 * This is the opposite of the get_next_member function.
	 *
	 * @param  MEMBER			The member id to decrement
	 * @return ?MEMBER		The previous member id (NULL: no previous member)
	 */
	function get_previous_member($member)
	{
		$tempid=$this->connection->query_value_null_ok_full('SELECT id FROM '.$this->connection->get_table_prefix().'f_members WHERE id<'.strval((integer)$member).' AND id>0 ORDER BY id DESC');
		if ($tempid==$this->get_guest_id()) return NULL;
		return $tempid;
	}
	
	/**
	 * Get the member id of the next member after the given one, or NULL.
	 * It cannot be assumed there are no gaps in member ids, as members may be deleted.
	 *
	 * @param  MEMBER			The member id to increment
	 * @return ?MEMBER		The next member id (NULL: no next member)
	 */
	function get_next_member($member)
	{
		$tempid=$this->connection->query_value_null_ok_full('SELECT id FROM '.$this->connection->get_table_prefix().'f_members WHERE id>'.strval((integer)$member).' ORDER BY id');
		return $tempid;
	}

	/**
	 * Try to find a member with the given IP address
	 *
	 * @param  IP				The IP address
	 * @return array			The distinct rows found
	 */
	function probe_ip($ip)
	{
		if (strpos($ip,'*')!==false)
		{
			$a=$this->connection->query('SELECT DISTINCT id FROM '.$this->connection->get_table_prefix().'f_members WHERE m_ip_address LIKE \''.db_encode_like(str_replace('*','%',$ip)).'\'');
			$b=$this->connection->query('SELECT DISTINCT p_poster AS id FROM '.$this->connection->get_table_prefix().'f_posts WHERE p_ip_address LIKE \''.db_encode_like(str_replace('*','%',$ip)).'\'');
		} else
		{
			$a=$this->connection->query_select('f_members',array('DISTINCT id'),array('m_ip_address'=>$ip));
			$b=$this->connection->query('SELECT DISTINCT p_poster AS id FROM '.$this->connection->get_table_prefix().'f_posts WHERE '.db_string_equal_to('p_ip_address',$ip));
		}
		return array_merge($a,$b);
	}

	/**
	 * Get the name relating to the specified member id.
	 * If this returns NULL, then the member has been deleted. Always take potential NULL output into account.
	 *
	 * @param  MEMBER			The member id
	 * @return ?SHORT_TEXT	The member name (NULL: member deleted)
	 */
	function _get_username($member)
	{
		if ($member==$this->get_guest_id()) return do_lang('GUEST');
		return $this->get_member_row_field($member,'m_username');
	}
	
	/**
	 * Get the e-mail address for the specified member id.
	 *
	 * @param  MEMBER			The member id
	 * @return SHORT_TEXT	The e-mail address
	 */
	function _get_member_email_address($member)
	{
		return $this->get_member_row_field($member,'m_email_address');
	}
	
	/**
	 * Get the photo thumbnail URL for the specified member id.
	 *
	 * @param  MEMBER			The member id
	 * @return URLPATH		The URL (blank: none)
	 */
	function get_member_photo_url($member)
	{
		if (!addon_installed('ocf_member_photos'))
		{
			if (!addon_installed('ocf_member_avatars')) return '';
			return $this->get_member_avatar_url($member);
		}

		$pic=$this->get_member_row_field($member,'m_photo_thumb_url');

		if ($pic=='')
		{
			$photo_url=$GLOBALS['FORUM_DRIVER']->get_member_row_field($member,'m_photo_url');
			if ($photo_url!='')
			{
				require_code('images');
				$pic=ensure_thumbnail($photo_url,$pic,(strpos($photo_url,'uploads/photos')!==false)?'photos':'ocf_photos','f_members',$member,'m_photo_thumb_url');
			}
		}

		if (is_null($pic)) $pic='';
		elseif ((url_is_local($pic)) && ($pic!='')) $pic=get_complex_base_url($pic).'/'.$pic;
		
		return $pic;
	}

	/**
	 * Get the avatar URL for the specified member id.
	 *
	 * @param  MEMBER			The member id
	 * @return URLPATH		The URL (blank: none)
	 */
	function get_member_avatar_url($member)
	{
		if ((!addon_installed('ocf_member_avatars')) && (!addon_installed('ocf_member_photos'))) return '';

		/*if (!addon_installed('ocf_member_avatars'))	Actually when photo is chosen, avatar is set - and will have been resized right
		{
			if (!addon_installed('ocf_member_photos')) return '';
			return $this->get_member_photo_url($member);
		}*/

		$avatar=$this->get_member_row_field($member,'m_avatar_url');
		if (is_null($avatar))
		{
			$avatar='';
		} else
		{
			$base_url=get_base_url();
			if (substr($avatar,0,strlen($base_url)+1)==$base_url.'/') // So we can do an is_file check
			{
				$avatar=substr($avatar,strlen($base_url)+1);
			}

			if ((url_is_local($avatar)) && ($avatar!=''))
			{
				if ((is_file(get_file_base().'/'.rawurldecode($avatar))) || (is_file(get_custom_file_base().'/'.rawurldecode($avatar))))
				{
					$avatar=get_complex_base_url($avatar).'/'.$avatar;
				} else
				{
					$avatar='';
				}
			}
		}

		return cdn_filter($avatar);
	}

	/**
	 * Find if this member may have e-mails sent to them
	 *
	 * @param  MEMBER			The member id
	 * @return boolean		Whether the member may have e-mails sent to them
	 */
	function get_member_email_allowed($member)
	{
		if (get_option('allow_email_disable')=='0') return true;
		return $this->get_member_row_field($member,'m_allow_emails');
	}

	/**
	 * Get the timestamp of a member's join date.
	 *
	 * @param  MEMBER			The member id
	 * @return TIME			The timestamp
	 */
	function get_member_join_timestamp($member)
	{
		return $this->get_member_row_field($member,'m_join_time');
	}

	/**
	 * Find all members with a name matching the given SQL LIKE string.
	 *
	 * @param  string			The pattern
	 * @param  ?integer		Maximum number to return (limits to the most recent active) (NULL: no limit)
	 * @return ?array			The array of matched members (NULL: none found)
	 */
	function get_matching_members($pattern,$limit=NULL)
	{
		$like='m_username LIKE \''.db_encode_like($pattern).'\' AND ';
		if (($pattern=='') || ($pattern=='%')) $like='';
		$rows=$this->connection->query('SELECT * FROM '.$this->connection->get_table_prefix().'f_members WHERE '.$like.'id<>'.strval($this->get_guest_id()).' ORDER BY m_last_submit_time DESC',$limit);

		global $M_SORT_KEY;
		$M_SORT_KEY='m_username';
		uasort($rows,'multi_sort');

		return $rows;
	}

	/**
	 * Get the given member's post count.
	 *
	 * @param  MEMBER			The member id
	 * @return integer		The post count
	 */
	function get_post_count($member)
	{
		return $this->get_member_row_field($member,'m_cache_num_posts');
	}

	/**
	 * Get the given member's topic count.
	 *
	 * @param  MEMBER			The member id
	 * @return integer		The topic count
	 */
	function get_topic_count($member)
	{
		return $this->connection->query_value('f_topics','COUNT(*)',array('t_cache_first_member_id'=>$member));
	}
	
	/**
	 * Find out if the given member id is banned.
	 *
	 * @param  MEMBER			The member id
	 * @return boolean		Whether the member is banned
	 */
	function is_banned($member)
	{
		return $this->get_member_row_field($member,'m_is_perm_banned')==1;
	}
	
	/**
	 * Find if the specified member id is marked as staff or not.
	 *
	 * @param  MEMBER			The member id
	 * @return boolean		Whether the member is staff
	 */
	function _is_staff($member)
	{
		if ($member==$this->get_guest_id()) return false;
		$users_groups=$this->get_members_groups($member);
		return ((ocf_get_best_group_property($users_groups,'is_super_moderator')==1) || (ocf_get_best_group_property($users_groups,'is_super_admin')==1));
	}

	/**
	 * Find if the specified member id is marked as a super admin or not.
	 *
	 * @param  MEMBER			The member id
	 * @return boolean		Whether the member is a super admin
	 */
	function _is_super_admin($member)
	{
		if ($member==$this->get_guest_id()) return false;
		$users_groups=$this->get_members_groups($member);
		return ocf_get_best_group_property($users_groups,'is_super_admin')==1;
	}

	/**
	 * Get the number of members currently online on the forums.
	 *
	 * @return ?integer		The number of members (NULL: the same as the site number)
	 */
	function get_num_users_forums()
	{
		return NULL; // Same as site
	}
	
	/**
	 * Get the number of members registered on the forum.
	 *
	 * @return integer		The number of members
	 */
	function get_members()
	{
		$value=intval(get_value_newer_than('ocf_member_count',time()-60*60*3));

		if ($value==0)
		{
			$value=$this->connection->query_value('f_members','COUNT(*)')-1;
			set_value('ocf_member_count',strval($value));
		}

		return $value;
	}

	/**
	 * Get the total topics ever made on the forum.
	 *
	 * @return integer		The number of topics
	 */
	function get_topics()
	{
		$value=intval(get_value_newer_than('ocf_topic_count',time()-60*60*3));

		if ($value==0)
		{
			$value=$this->connection->query_value('f_topics','COUNT(*)');
			set_value('ocf_topic_count',strval($value));
		}

		return $value;
	}

	/**
	 * Get the total posts ever made on the forum.
	 *
	 * @return integer		The number of posts
	 */
	function get_num_forum_posts()
	{
		$value=intval(get_value_newer_than('ocf_post_count',time()-60*60*3));

		if ($value==0)
		{
			$value=$this->connection->query_value('f_posts','COUNT(*)');
			set_value('ocf_post_count',strval($value));
		}
	
		return $value;
	}
	
	/**
	 * Get the number of new forum posts.
	 *
	 * @return integer		The number of posts
	 */
	function _get_num_new_forum_posts()
	{
		return $this->connection->query_value_null_ok_full('SELECT COUNT(*) FROM '.$this->connection->get_table_prefix().'f_posts WHERE p_time>'.strval(time()-60*60*24));
	}
	
	/**
	 * Get a member id from the given member's username.
	 *
	 * @param  SHORT_TEXT	The member name
	 * @return ?MEMBER		The member id (NULL: not found)
	 */
	function get_member_from_username($name)
	{
		foreach ($this->MEMBER_ROWS_CACHED as $id=>$row)
		{
			if ($row['m_username']==$name) return $id;
		}
		$row=$this->connection->query_select('f_members',array('*'),array('m_username'=>$name),'',1);
		if (!array_key_exists(0,$row)) return NULL;
		$id=$row[0]['id'];
		$this->MEMBER_ROWS_CACHED[$id]=$row[0];
		return $id;
	}
	
	/**
	 * Get a member id from the given member's username.
	 *
	 * @param  SHORT_TEXT	The member email address
	 * @return ?MEMBER		The member id (NULL: not found)
	 */
	function get_member_from_email_address($email_address)
	{
		foreach ($this->MEMBER_ROWS_CACHED as $id=>$row)
		{
			if ($row['m_email_address']==$email_address) return $id;
		}
		$row=$this->connection->query_select('f_members',array('*'),array('m_email_address'=>$email_address),'',1);
		if (!array_key_exists(0,$row)) return NULL;
		$id=$row[0]['id'];
		$this->MEMBER_ROWS_CACHED[$id]=$row[0];
		return $id;
	}
	
	/**
	 * Get the ids of the admin usergroups.
	 *
	 * @return array			The admin usergroup ids
	 */
	function _get_super_admin_groups()
	{
		return collapse_1d_complexity('id',$this->connection->query_select('f_groups',array('id'),array('g_is_super_admin'=>1)));
	}
	
	/**
	 * Get the ids of the moderator usergroups.
	 * It should not be assumed that a member only has one usergroup - this depends upon the forum the driver works for. It also does not take the staff site filter into account.
	 *
	 * @return array			The moderator usergroup ids
	 */
	function _get_moderator_groups()
	{
		return collapse_1d_complexity('id',$this->connection->query_select('f_groups',array('id'),array('g_is_super_moderator'=>1)));
	}
	
	/**
	 * Get the forum usergroup list. This is useful to enumerate usergroups, or to find usergroup names.
	 *
	 * @param  boolean		Whether to obscure the name of hidden usergroups
	 * @param  boolean		Whether to only grab permissive usergroups
	 * @param  boolean		Do not limit things even if there are huge numbers of usergroups
	 * @param  ?array			Usergroups that must be included in the results (NULL: no extras must be)
	 * @param  ?MEMBER		Always return usergroups of this member (NULL: current member)
	 * @param  boolean		Whether to completely skip hidden usergroups
	 * @return array			The usergroup list, a map of usergroup ID to usergroup name
	 */
	function _get_usergroup_list($hide_hidden=false,$only_permissive=false,$force_show_all=false,$force_find=NULL,$for_member=NULL,$skip_hidden=false)
	{
		if (($hide_hidden) && (has_specific_permission(get_member(),'see_hidden_groups'))) $hide_hidden=false;

		$where=$only_permissive?' WHERE g_is_private_club=0':'';

		$select='g.id,text_original,g_name';
		if ($hide_hidden) $select.=',g.g_hidden';
		$sup=' ORDER BY g_order,g.id';
		if (running_script('upgrader')) $sup='';
		$count=$this->connection->query_value_null_ok_full('SELECT COUNT(*) FROM '.$this->connection->get_table_prefix().'f_groups g'.$where);
		if (($count>100) && ((!$force_show_all) || ($count>4000)))
		{
			if (is_null($force_find)) $force_find=NULL;
			if (is_null($for_member)) $for_member=get_member();
			$where=' WHERE g_is_private_club=0';
			if (is_null($force_find)) $force_find=array();
			$force_find+=$this->_get_members_groups($for_member);
			foreach ($force_find as $gid)
			{
				$where.=' OR g.id='.strval($gid);
			}
		}
		if (!function_exists('require_lang')) require_code('lang');
		$query='SELECT '.$select.' FROM '.$this->connection->get_table_prefix().'f_groups g LEFT JOIN '.$this->connection->get_table_prefix().'translate t ON '.db_string_equal_to('language',user_lang()).' AND g.g_name=t.id'.$where.$sup;
		$rows=$this->connection->query($query);
		if ($hide_hidden)
		{
			require_lang('ocf');
		}
		$out=array();
		foreach ($rows as $row)
		{
			if ($GLOBALS['RECORD_LANG_STRINGS_CONTENT'] || is_null($row['text_original'])) $row['text_original']=get_translated_text($row['g_name'],$GLOBALS['FORUM_DB']);
			
			if (($hide_hidden) && ($row['g_hidden']==1))
			{
				if ($skip_hidden) continue;
				$out[$row['id']]=do_lang('SECRET_GROUP',strval($row['id']));
			} else
			{
				$out[$row['id']]=$row['text_original'];
			}
		}
		return $out;
	}

	/**
	 * Get the forum usergroup relating to the specified member id.
	 *
	 * @param  MEMBER			The member id
	 * @param  boolean		Whether to skip looking at secret usergroups.
	 * @param  boolean		Whether to take probation into account
	 * @return array			The array of forum usergroups
	 */
	function _get_members_groups($member,$skip_secret=false,$handle_probation=true)
	{
		require_code('ocf_groups');
		return array_keys(ocf_get_members_groups($member,$skip_secret,$handle_probation));
	}

	/**
	 * Create a member login cookie.
	 *
	 * @param  MEMBER			The member id
	 * @param  ?SHORT_TEXT	The username (NULL: lookup)
	 * @param  string			The password
	 */
	function forum_create_cookie($id,$name,$password)
	{
		unset($name);
		unset($password);

		// User
		ocp_setcookie(get_member_cookie(),strval($id));
		$_COOKIE[get_member_cookie()]=strval($id);
	
		// Password
		$password_hashed_salted=$this->get_member_row_field($id,'m_pass_hash_salted');
		$password_compat_scheme=$this->get_member_row_field($id,'m_password_compat_scheme');
		if ($password_compat_scheme=='plain') $password_hashed_salted=md5($password_hashed_salted); // can't do direct representation for this, would be a plain text cookie; so in forum_authorise_login we expect it to be md5'd and compare thusly (as per non-cookie call to that function)
		ocp_setcookie(get_pass_cookie(),$password_hashed_salted);
		$_COOKIE[get_pass_cookie()]=$password_hashed_salted;
	}
	
	/**
	 * The hashing algorithm of this forum driver.
	 *
	 * @param  string			The data to hash (the password in actuality)
	 * @param  SHORT_TEXT	The username
	 * @return string			The hashed data
	 */
	function forum_md5($password,$username)
	{
		/*if ((strpos($username,'.')!==false) && (strpos($username,'@')!==false))
		{
			return $password;
		}*/

		require_code('ocf_members');

		$user_id=$this->get_member_from_username($username);
		if (((is_null($GLOBALS['LDAP_CONNECTION'])) || (!ocf_is_on_ldap($username))) && (is_null($user_id)))
		{
			$user_id=$this->connection->query_value_null_ok('f_members','id',array('m_email_address'=>$username));
			if (is_null($user_id))
			{
				return '!'; // Invalid user logging in
			}
		}

		if ((!ocf_is_ldap_member($user_id)) && (!is_null($user_id)))
		{
			return md5($password);
		} else
		{
			return $password; //ocf_ldap_hash($user_id,$password); Can't do hash checks under all systems
		}
	}
	
	/**
	 * Find if the given member id and password is valid. If username is NULL, then the member id is used instead.
	 * All authorisation, cookies, and form-logins, are passed through this function.
	 * Some forums do cookie logins differently, so a Boolean is passed in to indicate whether it is a cookie login.
	 *
	 * @param  ?SHORT_TEXT	The member username (NULL: don't use this in the authentication - but look it up using the ID if needed)
	 * @param  ?MEMBER		The member id (NULL: use member name)
	 * @param  MD5				The md5-hashed password
	 * @param  string			The raw password
	 * @param  boolean		Whether this is a cookie login, determines how the hashed password is treated for the value passed in
	 * @return array			A map of 'id' and 'error'. If 'id' is NULL, an error occurred and 'error' is set
	 */
	function forum_authorise_login($username,$userid,$password_hashed,$password_raw,$cookie_login=false)
	{
		$out=array();
		$out['id']=NULL;
	
		require_code('ocf_members');
		require_code('ocf_groups');
		if (!function_exists('require_lang')) require_code('lang');
		if (!function_exists('do_lang_tempcode')) require_code('tempcode');
		if (!function_exists('require_lang')) return $out;
		require_lang('ocf');
		require_code('mail');
	
		$skip_auth=false;

		if ($userid===NULL)
		{
			$rows=$this->connection->query('SELECT * FROM '.$this->connection->get_table_prefix().'f_members WHERE '.db_string_equal_to('m_username',$username),1);
			if ((!array_key_exists(0,$rows)) && (get_option('one_per_email_address')=='1'))
			{
				$rows=$this->connection->query('SELECT * FROM '.$this->connection->get_table_prefix().'f_members WHERE '.db_string_equal_to('m_email_address',$username).' ORDER BY id ASC',1);
			}
			if (array_key_exists(0,$rows))
			{
				$this->MEMBER_ROWS_CACHED[$rows[0]['id']]=$rows[0];
				$userid=$rows[0]['id'];
			}
		} else
		{
			$rows[0]=$this->get_member_row($userid);
		}

		// LDAP to the rescue if we couldn't get a row
		global $LDAP_CONNECTION;
		if ((!array_key_exists(0,$rows)) && ($LDAP_CONNECTION!==NULL) && ($userid===NULL))
		{
			// See if LDAP has it -- if so, we can add
			$test=ocf_is_on_ldap($username);
			if (!$test)
			{
				$out['error']=(do_lang_tempcode('_USER_NO_EXIST',escape_html($username)));
				return $out;
			}

			$test_auth=ocf_ldap_authorise_login($username,$password_raw);
			if ($test_auth['m_pass_hash_salted']=='!!!')
			{
				$out['error']=(do_lang_tempcode('USER_BAD_PASSWORD'));
				return $out;
			}

			if ($test)
			{
				require_code('ocf_members_action');
				require_code('ocf_members_action2');
				$completion_form_submitted=(trim(post_param('email_address',''))!='');
				if ((!$completion_form_submitted) && (get_value('no_finish_profile')!=='1')) // UI
				{
					@ob_end_clean();
					if (!function_exists('do_header')) require_code('site');
					$middle=ocf_member_external_linker_ask($username,'ldap',ocf_ldap_guess_email($username));
					$tpl=globalise($middle,NULL,'',true);
					$tpl->evaluate_echo();
					exit();
				} else
				{
					$userid=ocf_member_external_linker($username,uniqid(''),'ldap');
					$row=$this->get_member_row($userid);
				}
			}
		}

		if ((!array_key_exists(0,$rows)) || ($rows[0]===NULL)) // All hands to lifeboats
		{
			$out['error']=(do_lang_tempcode('_USER_NO_EXIST',escape_html($username)));
			return $out;
		}
		$row=$rows[0];

		// Now LDAP can kick in and get the correct hash
		if (ocf_is_ldap_member($userid))
		{
			//$rows[0]['m_pass_hash_salted']=ocf_get_ldap_hash($userid);
	
			// Doesn't exist any more? This is a special case - the 'LDAP member' exists in our DB, but not LDAP. It has been deleted from LDAP or LDAP server has jumped
			/*if (is_null($rows[0]['m_pass_hash_salted']))
			{
				$out['error']=(do_lang_tempcode('_USER_NO_EXIST',$username));
				return $out;
			} No longer appropriate with new authentication mode - instead we just have to give an invalid password message  */

			$row=array_merge($row,ocf_ldap_authorise_login($username,$password_hashed));
		}

		if (addon_installed('unvalidated'))
		{
			if ($row['m_validated']==0)
			{
				$out['error']=(do_lang_tempcode('USER_NOT_VALIDATED_STAFF'));
				return $out;
			}
		}
		if ($row['m_validated_email_confirm_code']!='')
		{
			$out['error']=(do_lang_tempcode('USER_NOT_VALIDATED_EMAIL'));
			return $out;
		}
		if ($this->is_banned($row['id'])) // All hands to the guns
		{
			$out['error']=(do_lang_tempcode('USER_BANNED'));
			return $out;
		}
	
		// Check password
		if (!$skip_auth)
		{
			// Choose a compatibility screen.
			// Note that almost all cookie logins are the same. This is because the cookie logins use OCF cookies, regardless of compatibility scheme.
			$password_compatibility_scheme=$row['m_password_compat_scheme'];
			switch ($password_compatibility_scheme)
			{
				case 'remote': // This will work too - we're logging in with the username of a remote profile, so no resynching will happen
				case '': // ocPortal style salted MD5 algorithm
					if ($cookie_login)
					{
						if ($password_hashed!=$row['m_pass_hash_salted'])
						{
							require_code('tempcode'); // This can be incidental even in fast AJAX scripts, if an old invalid cookie is present, so we need tempcode for do_lang_tempcode
							$out['error']=(do_lang_tempcode('USER_BAD_PASSWORD'));
							return $out;
						}
					} else
					{
						if (md5($row['m_pass_salt'].$password_hashed)!=$row['m_pass_hash_salted'])
						{
							$out['error']=(do_lang_tempcode('USER_BAD_PASSWORD'));
							return $out;
						}
					}
					break;
				case 'plain':
					if ($password_hashed!=md5($row['m_pass_hash_salted']))
					{
						$out['error']=(do_lang_tempcode('USER_BAD_PASSWORD'));
						return $out;
					}
					break;
				case 'md5': // Old style plain md5		(also works if both are unhashed: used for LDAP)
					if (($password_hashed!=$row['m_pass_hash_salted']) && ($password_hashed!='!!!')) // The !!! bit would never be in a hash, but for plain text checks using this same code, we sometimes use '!!!' to mean 'Error'.
					{
						$out['error']=(do_lang_tempcode('USER_BAD_PASSWORD'));
						return $out;
					}
					break;
		/*		case 'httpauth':
					// This is handled in get_member()  */
					break;
				case 'ldap':
					if ($password_hashed!=$row['m_pass_hash_salted'])
					{
						$out['error']=(do_lang_tempcode('USER_BAD_PASSWORD'));
						return $out;
					}
					break;
				default:
					$path=get_file_base().'/sources_custom/hooks/systems/ocf_auth/'.$password_compatibility_scheme.'.php';
					if (!file_exists($path)) $path=get_file_base().'/sources/hooks/systems/ocf_auth/'.$password_compatibility_scheme.'.php';
					if (!file_exists($path))
					{
						$out['error']=(do_lang_tempcode('UNKNOWN_AUTH_SCHEME_IN_DB'));
						return $out;
					}
					require_code('hooks/systems/ocf_auth/'.$password_compatibility_scheme);
					$ob=object_factory('Hook_ocf_auth_'.$password_compatibility_scheme);
					$error=$ob->auth($username,$userid,$password_hashed,$password_raw,$cookie_login,$row);
					if (!is_null($error))
					{
						$out['error']=$error;
						return $out;
					}
					break;
			}
		}

		// Ok, authorised basically, but we need to see if this is a valid login IP
		if ((ocf_get_best_group_property($this->get_members_groups($row['id']),'enquire_on_new_ips')==1)) // High security usergroup membership
		{
			global $SENT_OUT_VALIDATE_NOTICE;
			$ip=get_ip_address(3);
			$test2=$this->connection->query_value_null_ok('f_member_known_login_ips','i_val_code',array('i_member_id'=>$row['id'],'i_ip'=>$ip));
			if (((is_null($test2)) || ($test2!='')) && (!compare_ip_address($ip,$row['m_ip_address'])))
			{
				if (!$SENT_OUT_VALIDATE_NOTICE)
				{
					if (!is_null($test2)) // Tidy up
					{
						$this->connection->query_delete('f_member_known_login_ips',array('i_member_id'=>$row['id'],'i_ip'=>$ip),'',1);
					}
	
					$code=!is_null($test2)?$test2:uniqid('');
					$this->connection->query_insert('f_member_known_login_ips',array('i_val_code'=>$code,'i_member_id'=>$row['id'],'i_ip'=>$ip));
					$url=find_script('validateip').'?code='.$code;
					$url_simple=find_script('validateip');
					$mail=do_lang('IP_VERIFY_MAIL',comcode_escape($url),comcode_escape(get_ip_address()),array($url_simple,$code),get_lang($row['id']));
					$email_address=$row['m_email_address'];
					if ($email_address=='') $email_address=get_option('staff_address');
					if (running_script('index'))
						mail_wrap(do_lang('IP_VERIFY_MAIL_SUBJECT',NULL,NULL,NULL,get_lang($row['id'])),$mail,array($email_address),$row['m_username'],'','',1);

					$SENT_OUT_VALIDATE_NOTICE=true;
				}

				$out['error']=do_lang_tempcode('REQUIRES_IP_VALIDATION');
				return $out;
			}
		}
	
		$this->ocf_flood_control($row['id']);

		$out['id']=$row['id'];
		return $out;
	}
	
	/**
	 * Handle flood control for members.
	 *
	 * @param  MEMBER			The member ID that just got detected
	 */
	function ocf_flood_control($id)
	{
		global $NON_PAGE_SCRIPT;
		if ($NON_PAGE_SCRIPT==1) return;

		global $FLOOD_CONTROL_ONCE;
		if ($FLOOD_CONTROL_ONCE) return;
		$FLOOD_CONTROL_ONCE=true;

		if (get_page_name()=='join') return;
		if ((!running_script('index')) && (!running_script('iframe'))) return;

		require_code('ocf_groups');

		// Set last visit time session cookie if it doesn't exist
		if ((!array_key_exists('last_visit',$_COOKIE)) && ($GLOBALS['FORUM_DRIVER']->get_guest_id()!=$id))
		{
			require_code('users_active_actions');
			$lvt=$this->get_member_row_field($id,'m_last_visit_time');
			ocp_setcookie('last_visit',is_null($lvt)?strval(time()):strval($lvt),true);
			$new_visit=true;
		} else
		{
			$new_visit=false;
		}

		// Do some flood control
		$submitting=((count($_POST)>0) && (get_param('type',NULL)!=='ed') && (get_param('type',NULL)!=='ec') && (!running_script('preview')));
		$restrict=$submitting?'flood_control_submit_secs':'flood_control_access_secs';
		$restrict_setting=$submitting?'m_last_submit_time':'m_last_visit_time';
		$restrict_answer=ocf_get_best_group_property($this->get_members_groups($id),$restrict);
		if ((!$submitting) && (array_key_exists('redirect',$_GET))) $restrict_answer=0;
		if ($restrict_answer<0) $restrict_answer=0;
		$last=$this->get_member_row_field($id,$restrict_setting);
		if ($last>time()) $last=time()-$restrict_answer; // Weird clock problem
		$wait_time=$restrict_answer-time()+$last;

		if (($wait_time>0) && (addon_installed('stats')))
		{
			require_code('site');
			log_stats('/flood',0);

			$time_threshold=30;
			$count_threshold=50;
			$query='SELECT COUNT(*) FROM '.$GLOBALS['SITE_DB']->get_table_prefix().'stats WHERE date_and_time>'.strval(time()-$time_threshold).' AND date_and_time<'.strval(time()).' AND '.db_string_equal_to('ip',get_ip_address());
			$count=$GLOBALS['SITE_DB']->query_value_null_ok_full($query);
			if (($count>=$count_threshold) && (addon_installed('securitylogging')))
			{
				$ip=get_ip_address();
				require_code('failure');
				add_ip_ban($ip);
				require_code('notifications');
				dispatch_notification('auto_ban',NULL,do_lang('AUTO_BAN_SUBJECT',$ip,NULL,NULL,get_site_default_lang()),do_lang('AUTO_BAN_DOS_MESSAGE',$ip,integer_format($count_threshold),integer_format($time_threshold),get_site_default_lang()),NULL,A_FROM_SYSTEM_PRIVILEGED);
			}
			if (!function_exists('require_lang')) require_code('lang');
			if (!function_exists('do_lang_tempcode')) require_code('tempcode');
			require_lang('ocf');

			warn_exit(do_lang_tempcode('FLOOD_CONTROL_RESTRICT',integer_format($wait_time)));
		}
		$extra=$submitting?array('m_last_submit_time'=>time()):array();

		$dif=time()-$this->get_member_row_field($id,'m_last_visit_time');
		if ($dif<0) $dif=0; // can happen if system clock changes
		if (is_guest($id)) // bit of a hack, so that guests don't trip each others limits. Works out statistically.
		{
			if (get_value('session_prudence')!=='1')
			{
				global $SESSION_CACHE;
				$num_guests=0;
				foreach ($SESSION_CACHE as $c)
				{
					if (!array_key_exists('the_user',$c)) continue; // Workaround to HipHop PHP weird bug

					if (($c['last_activity']>time()-60*4) && (is_guest($c['the_user']))) $num_guests++;
				}
				$dif*=$num_guests;
			} else $restrict_answer=0;
		}
		if (($submitting) || ((count($_POST)==0) && ($dif>$wait_time)))
		{
			if (($restrict_answer!=0) || ($dif>180) || ($new_visit))
			{
				$old_ip=$this->get_member_row_field($id,'m_ip_address');
			
				$change_map=array('m_last_visit_time'=>time());
				if (get_ip_address()!=$old_ip) $change_map['m_ip_address']=get_ip_address();

				if (get_db_type()!='xml')
					$this->connection->query_update('f_members',$change_map+$extra,array('id'=>$id),'',1,NULL,false,true);
			}
		}
	}

	/**
	 * Get a first known IP address of the given member.
	 *
	 * @param  MEMBER			The member id
	 * @return IP				The IP address
	 */
	function get_member_ip($id)
	{
		return $this->get_member_row_field($id,'m_ip_address');
	}
	
	/**
	 * Gets a whole member row from the database.
	 *
	 * @param  MEMBER			The member id
	 * @return ?array			The member row (NULL: no such member)
	 */
	function get_member_row($member)
	{
		if (isset($this->MEMBER_ROWS_CACHED[$member])) return $this->MEMBER_ROWS_CACHED[$member];
	
		$rows=$this->connection->query_select('f_members',array('*'),array('id'=>$member),'',1);
		if (!array_key_exists(0,$rows))
		{
			$this->MEMBER_ROWS_CACHED[$member]=NULL;
			return NULL;
		}
		$this->MEMBER_ROWS_CACHED[$member]=$rows[0];
		return $this->MEMBER_ROWS_CACHED[$member];
	}
	
	/**
	 * Gets a named field of a member row from the database.
	 *
	 * @param  MEMBER			The member id
	 * @param  string			The field identifier
	 * @return mixed			The field
	 */
	function get_member_row_field($member,$field)
	{
		$row=$this->get_member_row($member);
		return ($row===NULL)?NULL:$row[$field];
	}

}


