View file upload/includes/class_dm_threadpost.php

File size: 61.23Kb
<?php
/*======================================================================*\
|| #################################################################### ||
|| # vBulletin 4.0.5
|| # ---------------------------------------------------------------- # ||
|| # Copyright ©2000-2010 vBulletin Solutions Inc. All Rights Reserved. ||
|| # This file may not be redistributed in whole or significant part. # ||
|| # ---------------- VBULLETIN IS NOT FREE SOFTWARE ---------------- # ||
|| # http://www.vbulletin.com | http://www.vbulletin.com/license.html # ||
|| #################################################################### ||
\*======================================================================*/

if (!class_exists('vB_DataManager', false))
{
	exit;
}

require_once(DIR . '/includes/functions_newpost.php');

/**
* Base data manager for threads and posts. Uninstantiable.
*
* @package	vBulletin
* @version	$Revision: 35848 $
* @date		$Date: 2010-03-17 12:38:45 -0700 (Wed, 17 Mar 2010) $
*/
class vB_DataManager_ThreadPost extends vB_DataManager
{
	/**
	* If doing a flood check, this will hold the flood check object.
	* Needed for rollbacks.
	*
	* @var	null|vB_Floodcheck
	*/
	var $floodcheck = null;

	/**
	* If the post was marked as spam in pre-save, insert a row in postsave
	*
	* @var	boolean
	*/
	var $spamlog_insert = false;

	/**
	* Constructor - checks that the registry object has been passed correctly.
	*
	* @param	vB_Registry	Instance of the vBulletin data registry object - expected to have the database object as one of its $this->db member.
	* @param	integer		One of the ERRTYPE_x constants
	*/
	function vB_DataManager_ThreadPost(&$registry, $errtype = ERRTYPE_STANDARD)
	{
		if (!is_subclass_of($this, 'vB_DataManager_ThreadPost'))
		{
			trigger_error("Direct Instantiation of vB_DataManager_ThreadPost class prohibited.", E_USER_ERROR);
		}

		parent::vB_DataManager($registry, $errtype);
	}

	/**
	* Verifies that the specified user exists
	*
	* @param	integer	User ID
	*
	* @return 	boolean	Returns true if user exists
	*/
	function verify_userid(&$userid)
	{
		if ($userid == $this->registry->userinfo['userid'])
		{
			$this->info['user'] =& $this->registry->userinfo;
			$return = true;
		}
		else if ($userinfo = $this->dbobject->query_first_slave("SELECT * FROM " . TABLE_PREFIX . "user WHERE userid = $userid"))
		{
			$this->info['user'] =& $userinfo;
			$return = true;
		}
		else
		{
			$this->error('no_users_matched_your_query');
			$return = false;
		}

		if ($return == true)
		{
			if (isset($this->validfields['username']))
			{
				$this->do_set('username', $this->info['user']['username']);
			}
			else if (isset($this->validfields['postusername']))
			{
				$this->do_set('postusername', $this->info['user']['username']);
			}
		}

		return $return;
	}

	/**
	* Verifies the title is valid and sets up the title for saving (wordwrap, censor, etc).
	*
	* @param	string	Title text
	*
	* @param	bool	Whether the title is valid
	*/
	function verify_title(&$title)
	{
		// replace html-encoded spaces with actual spaces
		$title = preg_replace('/&#(0*32|x0*20);/', ' ', $title);

		$title = trim($title);

		if ($this->registry->options['titlemaxchars'] AND $title != $this->existing['title'])
		{
			if (!empty($this->info['show_title_error']))
			{
				if (($titlelen = vbstrlen($title)) > $this->registry->options['titlemaxchars'])
				{
					// title too long
					$this->error('title_toolong', $titlelen, $this->registry->options['titlemaxchars']);
					return false;
				}
			}
			else if (empty($this->info['is_automated']) OR !empty($this->info['chop_title']))
			{
				// not showing the title length error, just chop it
				$title = vbchop($title, $this->registry->options['titlemaxchars']);
			}
		}

		// censor, remove all caps subjects, and htmlspecialchars post title
		$title = htmlspecialchars_uni(fetch_no_shouting_text(fetch_censored_text($title)));

		// do word wrapping
		if ($this->registry->options['wordwrap'] != 0)
		{
			$title = fetch_word_wrapped_string($title);
		}

		return true;
	}

	/**
	* Verifies the page text is valid and sets it up for saving.
	*
	* @param	string	Page text
	*
	* @param	bool	Whether the text is valid
	*/
	function verify_pagetext(&$pagetext)
	{
		if (empty($this->info['is_automated']))
		{
			if ($this->registry->options['postmaxchars'] != 0 AND ($postlength = vbstrlen($pagetext)) > $this->registry->options['postmaxchars'])
			{
				$this->error('toolong', $postlength, $this->registry->options['postmaxchars']);
				return false;
			}

			$this->registry->options['postminchars'] = intval($this->registry->options['postminchars']);
			if ($this->registry->options['postminchars'] <= 0)
			{
				$this->registry->options['postminchars'] = 1;
			}
			if (vbstrlen(strip_bbcode($pagetext, $this->registry->options['ignorequotechars'])) < $this->registry->options['postminchars'])
			{
				$this->error('tooshort', $this->registry->options['postminchars']);
				return false;
			}
		}

		return parent::verify_pagetext($pagetext);

	}

	/**
	* Verifies that the icon selected is valid.
	*
	* @param	integer	The ID of the icon
	*
	* @return	bool	Whether the icon is valid
	*/
	function verify_iconid(&$iconid)
	{
		if ($iconid)
		{
			// try to improve permission checking on icons
			if (!$this->info['user'])
			{
				$userid = $this->fetch_field('userid');
				if (!$userid)
				{
					$userid = $this->fetch_field('postuserid');
				}

				$this->set_info('user', fetch_userinfo($userid));
			}

			if ($this->info['user'])
			{
				$membergroups = fetch_membergroupids_array($this->info['user']);
			}
			else
			{
				// this is assumed to be a guest; go magic numbers!
				$membergroups = array(1);
			}
			$imagecheck = $this->dbobject->query_read_slave("
				SELECT usergroupid FROM " . TABLE_PREFIX . "icon AS icon
				INNER JOIN " . TABLE_PREFIX . "imagecategorypermission USING (imagecategoryid)
				WHERE icon.iconid = $iconid
					AND usergroupid IN (" . $this->dbobject->escape_string(implode(',', $membergroups)) . ")
			");

			if ($this->dbobject->num_rows($imagecheck) == sizeof($membergroups))
			{
				$iconid = 0;
			}
		}

		return true;
	}

	/**
	* Fetches the amount of attachments associated with a posthash and user
	*
	* @param	string	Post hash
	* @param	integer	User ID associated with post hash (-1 means current user)
	*
	* @return	integer	Number of attachments
	*/
	function fetch_attachment_count($posthash, $userid = -1)
	{
		if ($userid == -1)
		{
			$userid = $this->fetch_field('userid', 'post');
		}
		$userid = intval($userid);

		$attachcount = $this->dbobject->query_first("
			SELECT COUNT(*) AS count
			FROM " . TABLE_PREFIX . "attachment
			WHERE
				posthash = '" . $this->dbobject->escape_string($posthash) . "'
					AND
				userid = $userid
		");

		return intval($attachcount['count']);
	}

	function insert_dupehash($threadid = -1)
	{
		if ($threadid == -1)
		{
			$threadid = $this->fetch_field('threadid');
		}

		$type = ($threadid > 0 ? 'reply' : 'thread');

		$forumid = $this->fetch_field('forumid');
		if (!$forumid)
		{
			$forumid = $this->info['forum']['forumid'];
		}

		$userid = $this->fetch_field('postuserid');
		if (!$userid)
		{
			$userid = $this->fetch_field('userid');
		}

		$dupehash = md5($forumid . $this->fetch_field('title') . $this->fetch_field('pagetext', 'post') . $userid . $type);

		/*insert query*/
		$this->dbobject->query_write("
			INSERT INTO " . TABLE_PREFIX . "posthash
			(userid, threadid, dupehash, dateline)
			VALUES
			(" . intval($userid) . ", " . intval($threadid) . ", '" . $dupehash . "', " . TIMENOW . ")
		");
	}

	/**
	* Inserts Post Log data for Akismet
	*
	* @return	void
	*/
	function insert_postlog_data()
	{
		if (empty($this->info['is_automated']))
		{
			$postid = intval($this->fetch_field($this->table == 'post' ? 'postid' : 'firstpostid'));

			/*insert query*/
			$this->dbobject->query_write("
				INSERT INTO " . TABLE_PREFIX . "postlog
				(postid, useragent, ip, dateline)
				VALUES
				(" . $postid . ", '" . $this->dbobject->escape_string(USER_AGENT) . "', " . sprintf('%u', ip2long(IPADDRESS)) . ", " . TIMENOW . ")
			");
		}
	}

	function akismet_mark_as_ham($postid)
	{
		$spamlog_check = $this->dbobject->query_first("SELECT * FROM " . TABLE_PREFIX . "spamlog WHERE postid = " . $postid);
		if (empty($spamlog_check))
		{ // Akismet doesn't appear to have really marked this as spam
			return;
		}

		$postdata = $this->dbobject->query_first("SELECT post.username AS username, post.pagetext AS pagetext, postlog.ip AS ip, postlog.useragent AS useragent FROM " . TABLE_PREFIX . "post AS post INNER JOIN " . TABLE_PREFIX . "postlog AS postlog ON(postlog.postid = post.postid) WHERE post.postid = " . $postid);
		if (!empty($postdata) AND !empty($this->registry->options['vb_antispam_key']))
		{
			require_once(DIR . '/includes/class_akismet.php');
			$akismet = new vB_Akismet($this->registry);
			$akismet->akismet_board = $this->registry->options['bburl'];
			$akismet->akismet_key = $this->registry->options['vb_antispam_key'];
			$akismet->mark_as_ham(array('user_ip' => $postdata['ip'], 'user_agent' => $postdata['useragent'], 'comment_type' => 'post', 'comment_author' => $postdata['username'], 'comment_content' => $postdata['pagetext']));
		}

		$this->dbobject->query_write("DELETE FROM " . TABLE_PREFIX . "spamlog WHERE postid = " . $postid);
	}

	function email_moderators($fields)
	{
		if ($this->info['skip_moderator_email'] OR !$this->info['forum'] OR in_coventry($this->fetch_field('userid', 'post'), true))
		{
			return;
		}

		$mod_emails = fetch_moderator_newpost_emails($fields, $this->info['forum']['parentlist'], $newpost_lang);

		if (!empty($mod_emails))
		{
			$foruminfo = $this->info['forum'];
			$foruminfo['title_clean'] = unhtmlspecialchars($foruminfo['title_clean']);

			$threadinfo = fetch_threadinfo($this->fetch_field('threadid'));

			require_once(DIR . '/includes/class_bbcode_alt.php');
			$plaintext_parser = new vB_BbCodeParser_PlainText($this->registry, fetch_tag_list());

			$email = ($this->info['user']['email'] ? $this->info['user']['email'] : $this->registry->userinfo['email']);
			$browsing_user = $this->registry->userinfo['username'];

			// ugly hack -- should be fixed in the future
			$this->registry->userinfo['username'] = unhtmlspecialchars($this->info['user']['username'] ? $this->info['user']['username'] : $this->registry->userinfo['username']);

			$post = array_merge($this->existing, $this->post);
			if (!$post['postid'])
			{
				$post['postid'] = $this->thread['firstpostid'];
			}

			require_once(DIR . '/includes/functions_misc.php');

			foreach ($mod_emails AS $toemail)
			{
				if ($toemail != $email)
				{
					$plaintext_parser->set_parsing_language(isset($newpost_lang["$toemail"]) ? $newpost_lang["$toemail"] : 0);
					$post['message'] = $plaintext_parser->parse($this->post['pagetext'], $foruminfo['forumid']);

					if ($threadinfo['prefixid'])
					{
						// need prefix in correct language
						$threadinfo['prefix_plain'] = fetch_phrase(
							"prefix_$threadinfo[prefixid]_title_plain",
							'global',
							'',
							false,
							true,
							isset($newpost_lang["$toemail"]) ? $newpost_lang["$toemail"] : 0,
							false
						) . ' ';
					}
					else
					{
						$threadinfo['prefix_plain'] = '';
					}

					$threadlink = fetch_seo_url('thread|nosession', $threadinfo);
					eval(fetch_email_phrases('moderator', iif(isset($newpost_lang["$toemail"]), $newpost_lang["$toemail"], 0)));
					vbmail($toemail, $subject, $message);
				}
			}

			// back to normal
			$this->registry->userinfo['username'] = htmlspecialchars_uni($browsing_user);
		}
	}

	function rebuild_keywords()
	{
		require_once(DIR . '/includes/functions_newpost.php');

		$threadinfo = array('taglist' => $this->fetch_field('taglist'), 'prefixid' => $this->fetch_field('prefixid'), 'title' => $this->fetch_field('title'));
		$keywords = fetch_keywords_list($threadinfo, (empty($this->info['pagetext']) ? $this->fetch_field('pagetext', 'post') : $this->info['pagetext']));

		$this->set('keywords', $keywords);
	}

	/**
	* This is a pre_save method that only applies to the subclasses that have post
	* fields as their members (ie, not _Thread). Likely only called in those class's
	* pre_save methods.
	*
	* @return	bool	True on success, false on failure
	*/
	function pre_save_post($doquery = true)
	{
		if ($this->info['forum']['podcast'] AND $this->info['podcasturl'] AND empty($this->info['podcastsize']))
		{
			require_once(DIR . '/includes/class_upload.php');
			$upload = new vB_Upload_Abstract($this->registry);
			if (!($this->info['podcastsize'] = intval($upload->fetch_remote_filesize($this->info['podcasturl']))))
			{
				$this->error('invalid_podcasturl');
				return false;
			}
		}

		if (!$this->condition)
		{
			if ($this->fetch_field('userid', 'post') == 0 AND $this->fetch_field('username', 'post') == '')
			{
				$this->error('nousername');
				return false;
			}

			if ($this->fetch_field('dateline', 'post') === null)
			{
				$this->set('dateline', TIMENOW);
			}

			if ($this->fetch_field('ipaddress', 'post') === null)
			{
				$this->set('ipaddress', ($this->registry->options['logip'] ? IPADDRESS : ''));
			}

			// flood check
			if ($this->registry->options['floodchecktime'] > 0 AND empty($this->info['preview']) AND empty($this->info['is_automated']) AND $this->fetch_field('userid', 'post'))
			{
				if (!$this->info['user'])
				{
					$this->info['user'] = fetch_userinfo($this->fetch_field('userid', 'post'));
				}
				$user =& $this->info['user'];

				if ($user['lastpost'] <= TIMENOW AND
					!can_moderate($this->info['forum']['forumid'], '', $user['userid'], $user['usergroupid'] . (trim($user['membergroupids']) ? ",$user[membergroupids]" : '')))
				{
					if (!class_exists('vB_FloodCheck', false))
					{
						require_once(DIR . '/includes/class_floodcheck.php');
					}
					$this->floodcheck = new vB_FloodCheck($this->registry, 'user', 'lastpost');
					$this->floodcheck->commit_key($this->registry->userinfo['userid'], TIMENOW, TIMENOW - $this->registry->options['floodchecktime']);
					if ($this->floodcheck->is_flooding())
					{
						$this->error('postfloodcheck', $this->registry->options['floodchecktime'], $this->floodcheck->flood_wait());
						return false;
					}

					if ($this->errors)
					{
						// if we already have errors, the save won't happen, so rollback now...
						$this->floodcheck->rollback();
					}
					else
					{
						// ...or, in case we have a new error
						$this->set_failure_callback(array(&$this->floodcheck, 'rollback'));
					}
				}
			}

		}

		if (!$this->verify_image_count('pagetext', 'allowsmilie', $this->info['forum']['forumid'], 'post'))
		{
			return false;
		}

		if ($this->info['posthash'])
		{
			$this->info['newattach'] = $this->fetch_attachment_count($this->info['posthash'], $this->fetch_field('userid', 'post'));
			$this->set('attach',
				intval($this->fetch_field('attach')) +
				$this->info['newattach']
			);
		}

		// New posts that aren't automated and are visible should be scanned
		if (!$this->condition AND !empty($this->registry->options['vb_antispam_key']) AND empty($this->info['is_automated']) AND $this->fetch_field('visible') == 1 AND (!$this->registry->options['vb_antispam_posts'] OR $this->registry->userinfo['posts'] < $this->registry->options['vb_antispam_posts']) AND !can_moderate())
		{
			require_once(DIR . '/includes/class_akismet.php');
			$akismet = new vB_Akismet($this->registry);
			$akismet->akismet_board = $this->registry->options['bburl'];
			$akismet->akismet_key = $this->registry->options['vb_antispam_key'];
			if ($akismet->verify_text(array('user_ip' => IPADDRESS, 'user_agent' => USER_AGENT, 'comment_type' => 'post', 'comment_author' => ($this->registry->userinfo['userid'] ? $this->registry->userinfo['username'] : $this->fetch_field('username', 'post')), 'comment_author_email' => $this->registry->userinfo['email'], 'comment_author_url' => $this->registry->userinfo['homepage'], 'comment_content' => $this->fetch_field('pagetext', 'post'))) === 'spam')
			{
				$this->set('visible', 0);
				$this->spamlog_insert = true;
			}
		}

		return true;
	}

	/**
	* Post save function run on each record. Applies only if there was a post submitted.
	*/
	function post_save_each_post($doquery = true)
	{
		$postid = intval($this->fetch_field($this->table == 'post' ? 'postid' : 'firstpostid'));

		if (!$this->info['user'] AND $this->registry->userinfo['userid'] AND $this->fetch_field('userid', 'post') == $this->registry->userinfo['userid'])
		{
			$this->set_info('user', $this->registry->userinfo);
		}

		if ($this->info['posthash'] AND $this->fetch_field('attach') AND $postid)
		{
			$this->dbobject->query_write("
				UPDATE " . TABLE_PREFIX . "attachment
				SET
					contentid = $postid,
					posthash = ''
				WHERE
					posthash = '" . $this->dbobject->escape_string($this->info['posthash']) . "'
						AND
					userid = " . intval($this->fetch_field('userid', 'post')) . "
			");
		}

		if ($this->condition AND $postid)
		{
			if ($this->post['pagetext'])
			{
				if ($this->fetch_field('userid', 'post') != $this->registry->userinfo['userid'])
				{ // if another user edits the post then the postlog information is no longer valid.
					$this->dbobject->query_write("DELETE FROM " . TABLE_PREFIX . "postlog WHERE postid = " . intval($postid));
				}

				$this->dbobject->query_write("DELETE FROM " . TABLE_PREFIX . "postparsed WHERE postid = " . intval($postid));

				if ($this->info['forum'])
				{
					require_once(DIR . '/includes/functions_databuild.php');
					delete_post_index($postid, $this->existing['title'], $this->existing['pagetext']);
				}
			}

			// Check to see if this was a spam post being approved
			if ($this->existing['visible'] == 0 AND $this->fetch_field('visible') == 1)
			{
				$this->akismet_mark_as_ham($postid);
			}
		}

		if ($this->post['pagetext'] AND $this->info['forum'] AND $postid)
		{
			// ### UPDATE SEARCH INDEX ###
			require_once(DIR . '/includes/functions_databuild.php');
			build_post_index($postid, $this->info['forum']);
		}

		if ($this->spamlog_insert AND $postid)
		{
			$this->dbobject->query_write("INSERT INTO " . TABLE_PREFIX . "spamlog (postid) VALUES ($postid)");
		}

		if (!$this->condition AND $this->fetch_field('visible') == 1)
		{
			if ($this->info['forum'] AND $this->fetch_field('dateline') == TIMENOW)
			{
				$forumdata =& datamanager_init('Forum', $this->registry, ERRTYPE_SILENT);
				$forumdata->set_existing($this->info['forum']);
				$forumdata->set_info('disable_cache_rebuild', true);

				if (in_coventry($this->fetch_field('userid', 'post'), true))
				{
					$forumdata->set_info(
						'coventry',
						array(
							'in_coventry'	=> 1,
							'userid'	=> $this->fetch_field('userid', 'post')
						)
					);
				}

				if ($this->table == 'thread')
				{
					// we're inserting a new thread
					$forumdata->set('threadcount', 'threadcount + 1', false);
				}

				$forumdata->set('replycount', 'replycount + 1', false);
				$forumdata->set('lastpost', $this->fetch_field('dateline'));
				$forumdata->set('lastpostid', $postid);
				$forumdata->set('lastposter', $this->fetch_field('username', 'post'));
				$forumdata->set('lastposterid', $this->fetch_field('userid', 'post'));

				if ($this->table == 'thread')
				{
					$forumdata->set('lastthread', $this->fetch_field('title'));
					$forumdata->set('lastthreadid', $this->fetch_field('threadid'));
					$forumdata->set('lasticonid', ($this->fetch_field('pollid') ? -1 : $this->fetch_field('iconid')));
					$forumdata->set('lastprefixid', $this->fetch_field('prefixid'));
				}
				else if ($this->info['thread'])
				{
					$forumdata->set('lastthread', $this->info['thread']['title']);
					$forumdata->set('lastthreadid', $this->info['thread']['threadid']);
					$forumdata->set('lasticonid', ($this->info['thread']['pollid'] ? -1 : $this->info['thread']['iconid']));
					$forumdata->set('lastprefixid', $this->info['thread']['prefixid']);
				}

				$forumdata->save();
			}

			if ($this->info['user'] AND (empty($this->info['is_automated']) OR $this->info['is_automated'] == 'rss'))
			{
				$user =& datamanager_init('User', $this->registry, ERRTYPE_SILENT);
				$user->set_existing($this->info['user']);

				if ($this->info['forum']['countposts'])
				{
					$user->set('posts', 'posts + 1', false);
					$user->set_ladder_usertitle($this->info['user']['posts'] + 1);
				}

				$dateline = $this->fetch_field('dateline');

				if ($dateline == TIMENOW OR (isset($this->info['user']['lastpost']) AND $dateline > $this->info['user']['lastpost']))
				{
					$user->set('lastpost', $dateline);
				}

				$postid = intval($this->fetch_field('postid'));

				if ($dateline == TIMENOW OR (isset($this->info['user']['lastpostid']) AND $postid > $this->info['user']['postid']))
				{
					$user->set('lastpostid', $postid);
				}

				$user->save();
			}
		}
	}
}

/**
* Class to do data save/delete operations for POSTS
*
* @package	vBulletin
* @version	$Revision: 35848 $
* @date		$Date: 2010-03-17 12:38:45 -0700 (Wed, 17 Mar 2010) $
*/
class vB_DataManager_Post extends vB_DataManager_ThreadPost
{
	/**
	* Array of recognised and required fields for posts, and their types
	*
	* @var	array
	*/
	var $validfields = array(
		'postid'         => array(TYPE_UINT, REQ_INCR,  'return ($data > 0);'),
		'threadid'       => array(TYPE_UINT, REQ_YES),
		'parentid'       => array(TYPE_UINT, REQ_AUTO),
		'username'       => array(TYPE_STR,  REQ_NO,    VF_METHOD),
		'userid'         => array(TYPE_UINT, REQ_NO,    VF_METHOD),
		'title'          => array(TYPE_STR,  REQ_NO,    VF_METHOD),
		'dateline'       => array(TYPE_UINT, REQ_AUTO),
		'pagetext'       => array(TYPE_STR,  REQ_YES,   VF_METHOD),
		'allowsmilie'    => array(TYPE_UINT, REQ_YES), // this is required as we must know whether smilies count as images
		'showsignature'  => array(TYPE_BOOL, REQ_NO),
		'ipaddress'      => array(TYPE_STR,  REQ_AUTO),
		'iconid'         => array(TYPE_UINT, REQ_NO,    VF_METHOD),
		'visible'        => array(TYPE_UINT, REQ_NO),
		'attach'         => array(TYPE_UINT, REQ_NO),
		'infraction'     => array(TYPE_UINT, REQ_NO),
		'reportthreadid' => array(TYPE_UINT, REQ_NO),
		'htmlstate'      => array(TYPE_STR, REQ_NO),
	);

	/**
	* Array of field names that are bitfields, together with the name of the variable in the registry with the definitions.
	*
	* @var	array
	*/
	var $bitfields = array();

	/**
	* The main table this class deals with
	*
	* @var	string
	*/
	var $table = 'post';

	/**
	* Condition template for update query
	* This is for use with sprintf(). First key is the where clause, further keys are the field names of the data to be used.
	*
	* @var	array
	*/
	var $condition_construct = array('postid = %1$d', 'postid');

	/**
	* Array to store stuff to save to post table
	*
	* @var	array
	*/
	var $post = array();

	/**
	* Constructor - checks that the registry object has been passed correctly.
	*
	* @param	vB_Registry	Instance of the vBulletin data registry object - expected to have the database object as one of its $this->db member.
	* @param	integer		One of the ERRTYPE_x constants
	*/
	function vB_DataManager_Post(&$registry, $errtype = ERRTYPE_STANDARD)
	{
		parent::vB_DataManager_ThreadPost($registry, $errtype);

		($hook = vBulletinHook::fetch_hook('postdata_start')) ? eval($hook) : false;
	}

	function pre_save($doquery = true)
	{
		if ($this->presave_called !== null)
		{
			return $this->presave_called;
		}

		if (!$this->pre_save_post($doquery))
		{
			$this->presave_called = false;
			return false;
		}

		if (!$this->condition AND $this->fetch_field('parentid') === null AND ($this->info['thread'] OR $this->post['threadid']))
		{
			// we're not posting a new thread, so make this post a child of the first post in the thread
			if ($this->info['thread']['firstpostid'])
			{
				$this->set('parentid', $this->info['thread']['firstpostid']);
			}
			else
			{
				$getfirstpost = $this->dbobject->query_first("SELECT postid FROM " . TABLE_PREFIX . "post WHERE threadid = " . $this->post['threadid'] . " ORDER BY dateline, postid LIMIT 1");
				$this->set('parentid', $getfirstpost['postid']);
			}
		}

		$return_value = true;

		($hook = vBulletinHook::fetch_hook('postdata_presave')) ? eval($hook) : false;

		// we've errored, so try to roll the floodcheck back if it happened
		if ($return_value == false AND !empty($this->floodcheck))
		{
			//$this->floodcheck->rollback();
		}

		$this->presave_called = $return_value;
		return $return_value;
	}

	/**
	 * Overridding parent function to add search index updates
	 *
	* @param	boolean	Do the query?
	* @param	mixed	Whether to run the query now; see db_update() for more info
	* @param bool 	Whether to return the number of affected rows.
	* @param bool		Perform REPLACE INTO instead of INSERT
	8 @param bool		Perfrom INSERT IGNORE instead of INSERT
	*
	* @return	mixed	If this was an INSERT query, the INSERT ID is returned
	*/
	function save($doquery = true, $delayed = false, $affected_rows = false, $replace = false, $ignore = false)
	{
		global $vbulletin;
		
		// Call and get the new id
		$result = parent::save($doquery, $delayed, $affected_rows, $replace, $ignore);

		if ($result AND ($this->post['postid'] OR $this->existing['postid']))
		{
			// If result is the number (opposed to just TRUE) then use that, or which ever of the others is a number
			$do = (is_bool($result) == true ? (is_numeric($this->existing['postid']) == true ? $this->existing['postid'] : $this->existing['postid']) : $result);

			// Search index maintenance. Use the new cron'd processing;
			require_once DIR  . '/vb/search/indexcontroller/queue.php' ;
			$msgid = intval($this->existing['postid']) > 0 ? $this->existing['postid'] : $this->post['postid'];
			$foruminfo = $this->info['forum'];
			
			// if post was in cms comment forum, index as a cms comment rather than thread post
			if (isset($vbulletin->options['vbcmsforumid']) AND $foruminfo['forumid'] == $vbulletin->options['vbcmsforumid'])
			{
				vb_Search_Indexcontroller_Queue::indexQueue('vBCms', 'CmsComment', 'index', $msgid);
			}
			else
			{
				vb_Search_Indexcontroller_Queue::indexQueue('vBForum', 'Post', 'index', $msgid);
			}
		}

		return $result;
	}

	function post_save_each($doquery = true)
	{
		$postid = intval($this->fetch_field('postid'));

		if (!$this->condition AND $this->fetch_field('dateline') == TIMENOW)
		{
			$this->insert_dupehash($this->fetch_field('threadid'));
		}

		$this->post_save_each_post($doquery);

		if ($this->info['thread'] AND ($attach = intval($this->info['newattach']) OR !$this->condition))
		{
			$thread =& datamanager_init('Thread', $this->registry, ERRTYPE_SILENT, 'threadpost');
			$thread->set_existing($this->info['thread']);

			if ($attach)
			{
				$thread->set('attach', "attach + $attach", false);
			}
		}

		if ($this->info['thread'] AND $this->info['thread']['firstpostid'] == $this->fetch_field('postid'))
		{
			if (!is_object($thread))
			{
				$thread =& datamanager_init('Thread', $this->registry, ERRTYPE_SILENT, 'threadpost');
				$thread->set_existing($this->info['thread']);
			}
			$thread->set_info('pagetext', $this->fetch_field('pagetext'));

			$thread->rebuild_keywords();
		}

		if (!$this->condition)
		{
			if ($this->fetch_field('dateline') == TIMENOW)
			{
				$this->insert_postlog_data();
			}

			if ($this->fetch_field('visible') == 1 AND $this->info['thread'])
			{
				if (in_coventry($this->fetch_field('userid'), true))
				{
					$thread->set_info(
						'coventry',
						array (
							'in_coventry'  => 1,
							'userid'       => $this->fetch_field('userid')
						)
					);
				}


				if ($this->fetch_field('dateline') == TIMENOW)
				{
					$thread->set('lastpost', TIMENOW);
					$thread->set('lastposter', $this->fetch_field('username'));
					$thread->set('lastposterid', $this->fetch_field('userid'));
					$thread->set('lastpostid', $postid);
				}

				// update last post info for this thread
				if ($this->info['thread']['replycount'] % 10 == 0)
				{
					$replies = $this->registry->db->query_first("
						SELECT COUNT(*)-1 AS replies
						FROM " . TABLE_PREFIX . "post AS post
						WHERE threadid = " . intval($this->info['thread']['threadid']) . " AND
							post.visible = 1
					");

					$thread->set('replycount', $replies['replies']);
				}
				else
				{
					$thread->set('replycount', 'replycount + 1', false);
				}


			}
			else if ($this->fetch_field('visible') == 0 AND $this->info['thread'])
			{
				$thread->set('hiddencount', 'hiddencount + 1', false);
			}

			/*if ($this->fetch_field('visible') == 1 AND !in_coventry($this->registry->userinfo['userid'], true))
			{
				// Send out subscription emails
				exec_send_notification($this->fetch_field('threadid'), $this->registry->userinfo['userid'], $this->fetch_field('postid'));
			}*/
		}

		if (is_object($thread))
		{
			$thread->save();
		}

		if ($this->post['visible'] === 0)
		{
			$threadid = intval($this->fetch_field('threadid'));
			$postid = intval($this->fetch_field('postid'));

			/*insert query*/
			$this->dbobject->query_write("INSERT IGNORE INTO " . TABLE_PREFIX . "moderation (primaryid, type, dateline) VALUES ($postid, 'reply', " . TIMENOW . ")");
		}

		if ($this->info['forum']['podcast'] AND $this->info['thread']['firstpostid'] == $postid)
		{
			$this->dbobject->query_write("
				REPLACE INTO " . TABLE_PREFIX . "podcastitem
					(postid, url, length, explicit, author, keywords, subtitle)
				VALUES
					(
						$postid,
						'" . $this->dbobject->escape_string($this->info['podcasturl']) . "',
						" . intval($this->info['podcastsize']) . ",
						" . intval($this->info['podcastexplicit']) . ",
						'" . $this->dbobject->escape_string($this->info['podcastauthor']) . "',
						'" . $this->dbobject->escape_string($this->info['podcastkeywords']) . "',
						'" . $this->dbobject->escape_string($this->info['podcastsubtitle']) . "'
					)
			");

			// reset rss cache for this forum
			$this->dbobject->query_write("
				DELETE FROM " . TABLE_PREFIX . "externalcache
				WHERE forumid = " . intval($this->info['forum']['forumid']) . "
			");
		}

		if (!$this->condition)
		{
			$this->email_moderators('newpostemail');
		}

		($hook = vBulletinHook::fetch_hook('postdata_postsave')) ? eval($hook) : false;
	}


	/**
	* Deletes a post
	*
	* @param	boolean	Whether to consider updating post counts, regardless of forum's settings
	* @param	integer Thread that this post belongs to
	* @param	boolean	Whether to physically remove the thread from the database
	* @param	array	Array of information for a soft delete
	*
	* @return	mixed	The number of affected rows
	*/
	function delete($countposts = true, $threadid = 0, $physicaldel = true, $delinfo = NULL, $dolog = true)
	{
		global $vbulletin;
		if ($postid = $this->existing['postid'])
		{
			require_once(DIR . '/includes/functions_databuild.php');
			// note: the skip_moderator_log is the inverse of the $dolog argument

			// Search index maintenance
			require_once(DIR . '/vb/search/indexcontroller/queue.php');

			// if post is cms comment, delete comment index record, otherwise delete forum post record
			if ($this->existing['forumid'] == $vbulletin->options['vbcmsforumid'])
			{
				vb_Search_Indexcontroller_Queue::indexQueue('vBCms', 'CmsComment', 'delete', $postid);
			}
			else
			{
				vb_Search_Indexcontroller_Queue::indexQueue('vBForum', 'Post', 'delete', $postid);
			}

			($hook = vBulletinHook::fetch_hook('postdata_delete')) ? eval($hook) : false;

			return delete_post($postid, $countposts, $threadid, $physicaldel, $delinfo, ($this->info['skip_moderator_log'] !== null ? !$this->info['skip_moderator_log'] : $dolog));
		}

		return false;
	}
}

/**
* Class to do data save/delete operations for THREADS. Primarily useful when
* updating a thread's settings and you don't want to bring the first post into
* the picture.
*
* @package	vBulletin
* @version	$Revision: 35848 $
* @date		$Date: 2010-03-17 12:38:45 -0700 (Wed, 17 Mar 2010) $
*/
class vB_DataManager_Thread extends vB_DataManager_ThreadPost
{
	/**
	* Array of recognised and required fields for threads, and their types
	*
	* @var	array
	*/
	var $validfields = array(
		'threadid'      => array(TYPE_UINT, REQ_INCR),
		'title'         => array(TYPE_STR,  REQ_YES,   VF_METHOD),
		'firstpostid'   => array(TYPE_UINT, REQ_NO),
		'lastpost'      => array(TYPE_UINT, REQ_NO),
		'forumid'       => array(TYPE_UINT, REQ_YES),
		'pollid'        => array(TYPE_UINT, REQ_NO),
		'open'          => array(TYPE_UINT, REQ_AUTO,  VF_METHOD),
		'replycount'    => array(TYPE_UINT, REQ_NO),
		'hiddencount'   => array(TYPE_UINT, REQ_NO),
		'deletedcount'  => array(TYPE_UINT, REQ_NO),
		'postusername'  => array(TYPE_STR,  REQ_NO,    VF_METHOD, 'verify_username'),
		'postuserid'    => array(TYPE_UINT, REQ_NO,    VF_METHOD, 'verify_userid'),
		'lastposter'    => array(TYPE_STR,  REQ_NO),
		'lastposterid'  => array(TYPE_UINT, REQ_NO),
		'lastpostid'    => array(TYPE_UINT, REQ_NO),
		'dateline'      => array(TYPE_UINT, REQ_AUTO),
		'views'         => array(TYPE_UINT, REQ_NO),
		'iconid'        => array(TYPE_UINT, REQ_NO,    VF_METHOD),
		'notes'         => array(TYPE_STR,  REQ_NO),
		'visible'       => array(TYPE_UINT, REQ_NO),
		'sticky'        => array(TYPE_UINT, REQ_NO,    VF_METHOD),
		'votenum'       => array(TYPE_UINT, REQ_NO),
		'votetotal'     => array(TYPE_UINT, REQ_NO),
		'attach'        => array(TYPE_UINT, REQ_NO),
		'similar'       => array(TYPE_STR,  REQ_AUTO),
		'prefixid'      => array(TYPE_STR,  REQ_NO,    VF_METHOD),
		'taglist'       => array(TYPE_STR,  REQ_NO),
		'keywords'      => array(TYPE_STR,  REQ_NO)
	);

	/**
	* Array of field names that are bitfields, together with the name of the variable in the registry with the definitions.
	*
	* @var	array
	*/
	var $bitfields = array();

	/**
	* The main table this class deals with
	*
	* @var	string
	*/
	var $table = 'thread';

	/**
	* Condition template for update query
	* This is for use with sprintf(). First key is the where clause, further keys are the field names of the data to be used.
	*
	* @var	array
	*/
	var $condition_construct = array('threadid = %1$d', 'threadid');

	/**
	* Array to store stuff to save to thread/post tables
	*
	* @var	array
	*/
	var $thread = array();

	/**
	* Array holding moderator log details to insert
	*
	* @var	array
	*/
	var $modlog = array();

	/**
	* Constructor - checks that the registry object has been passed correctly.
	*
	* @param	vB_Registry	Instance of the vBulletin data registry object - expected to have the database object as one of its $this->db member.
	* @param	integer		One of the ERRTYPE_x constants
	*/
	function vB_DataManager_Thread(&$registry, $errtype = ERRTYPE_STANDARD)
	{
		parent::vB_DataManager_ThreadPost($registry, $errtype);

		($hook = vBulletinHook::fetch_hook('threaddata_start')) ? eval($hook) : false;
	}

	/**
	* Takes valid data and sets it as part of the data to be saved
	*
	* @param	string	The name of the field to which the supplied data should be applied
	* @param	mixed	The data itself
	*/
	function do_set($fieldname, &$value)
	{
		switch($fieldname)
		{
			case 'lastpost':
			case 'lastposter':
			case 'lastpostid':
			case 'lastposterid':
			{
				if (!empty($this->info['coventry']) AND $this->info['coventry']['in_coventry'] == 1)
				{
					$table = 'tachythreadpost';
				}
				else
				{
					$table = $this->table;
				}
			}
			break;

			case 'replycount':
			{
				if (!empty($this->info['coventry']) AND $this->info['coventry']['in_coventry'] == 1)
				{
					$table = 'tachythreadcounter';

				}
				else
				{
					$table = $this->table;

				}
			}
			break;

			default:
			{
				$table = $this->table;
			}
		}

		$this->setfields["$fieldname"] = true;
		$this->{$table}["$fieldname"] =& $value;
	}


	/**
	* Verifies the title. Does the same processing as the general title verifier,
	* but also requires there be a title.
	*
	* @param	string	Title text
	*
	* @return	bool	Whether the title is valid
	*/
	function verify_title(&$title)
	{
		if (!parent::verify_title($title))
		{
			return false;
		}

		if ($title == '')
		{
			$this->error('nosubject');
			return false;
		}

		if ($this->condition AND !$this->info['skip_moderator_log'] AND $title != $this->existing['title'])
		{
			require_once(DIR . '/includes/functions_log_error.php');
			$logtype = fetch_modlogtypes('thread_title_x_changed');
			$this->modlog[] = array('userid' => intval($this->registry->userinfo['userid']), 'type' => intval($logtype), 'action' => $this->existing['title']);
		}

		return true;
	}

	function verify_open(&$open)
	{
		if (!in_array($open, array(0, 1, 10)))
		{
			$open = 1;
		}

		if ($this->condition AND !$this->info['skip_moderator_log'])
		{
			require_once(DIR . '/includes/functions_log_error.php');
			if ($this->fetch_field('open'))
			{
				$logtype = fetch_modlogtypes('closed_thread');
			}
			else
			{
				$logtype = fetch_modlogtypes('opened_thread');
			}
			$this->modlog[] = array('userid' => intval($this->registry->userinfo['userid']), 'type' => intval($logtype));
		}

		return true;
	}

	function verify_sticky(&$sticky)
	{
		if ($sticky != 1)
		{
			$sticky = 0;
		}

		if ($this->condition AND !$this->info['skip_moderator_log'])
		{
			require_once(DIR . '/includes/functions_log_error.php');
			if ($this->fetch_field('sticky'))
			{
				$logtype = fetch_modlogtypes('unstuck_thread');
			}
			else
			{
				$logtype = fetch_modlogtypes('stuck_thread');
			}
			$this->modlog[] = array('userid' => intval($this->registry->userinfo['userid']), 'type' => intval($logtype));
		}

		return true;
	}

	function verify_prefixid(&$prefixid)
	{
		if ($prefixid === '')
		{
			return true;
		}

		if (!$this->registry->db->query_first("
			SELECT prefixid
			FROM " . TABLE_PREFIX . "prefix
			WHERE prefixid = '" . $this->registry->db->escape_string($prefixid) . "'
		"))
		{
			$prefixid = '';
		}

		return true;
	}

	//We need to index the changes
	//
	function post_save_once($doquery = true)
	{
		global $vbulletin;
		// do not index threads in the cms comment forum
		if ($this->fetch_field('forumid') == $vbulletin->options['vbcmsforumid'])
		{
			return;
		}
		
		static $saved = array();
		$threadid = $this->fetch_field('threadid');

		if (in_array($threadid, $saved))
		{
			return;
		}
		$saved[] = $threadid;

		require_once(DIR . '/vb/search/indexcontroller/queue.php');
		vb_Search_Indexcontroller_Queue::indexQueue('vBForum', 'Post', 'thread_data_change', $threadid);
	}

	function pre_save($doquery = true)
	{

		if ($this->presave_called !== null)
		{
			return $this->presave_called;
		}

		if ($this->thread['username'])
		{
			$this->do_set('postusername', $this->thread['username']);
		}
		if ($this->thread['userid'])
		{
			$this->do_set('postuserid', $this->thread['userid']);
		}

		//the condition constraint means that we don't trigger this if the record
		//hasn't been loaded (probably means that its new). We actually explicitly
		//add this code in most cases when we save the thread, so it might be worth
		//figuring out a better way to trigger this so that we don't have code
		//copied hither and yon.  However we also don't want to do it twice so I'm
		//not going to take the risk of changing it now.
		if (!$this->condition AND $this->registry->options['similarthreadsearch'])
		{
			require_once(DIR . '/vb/search/core.php');
			$searchcontroller = vB_Search_Core::get_instance()->get_search_controller();
			$similarthreads = $searchcontroller->get_similar_threads($this->fetch_field('title'));
			$this->set('similar', implode(',', $similarthreads));
		}

		if (!$this->condition)
		{
			if (!$this->fetch_field('dateline'))
			{
				$this->set('dateline', TIMENOW);
			}

			if ($this->fetch_field('open') === null)
			{
				$oldvalue = $this->info['skip_moderator_log'];
				$this->set_info('skip_moderator_log', true);
				$this->set('open', 1);
				$this->set_info('skip_moderator_log', $oldvalue);
			}
		}

		// updating prefix or forumid (with a prefix), we need to verify that a valid prefix was chosen
		if (!empty($this->thread['prefixid']) OR ($this->fetch_field('prefixid') AND !empty($this->thread['forumid'])))
		{
			if (!$this->registry->db->query_first("
				SELECT forumprefixset.forumid
				FROM " . TABLE_PREFIX . "prefix AS prefix
				INNER JOIN " . TABLE_PREFIX . "forumprefixset AS forumprefixset ON
					(prefix.prefixsetid = forumprefixset.prefixsetid AND forumprefixset.forumid = " . intval($this->fetch_field('forumid')) . ")
				WHERE prefix.prefixid = '" . $this->registry->db->escape_string($this->fetch_field('prefixid')) . "'
			"))
			{
				// selected prefix doesn't apply to this forum, blank it
				$this->set('prefixid', '');
			}
		}

		// no keywords set, and we have the pagetext
		if (empty($this->thread['keywords']) AND !empty($this->info['pagetext']))
		{
			$this->rebuild_keywords();
		}

		$return_value = true;
		($hook = vBulletinHook::fetch_hook('threaddata_presave')) ? eval($hook) : false;

		$this->presave_called = $return_value;

		return $return_value;
	}

	function insert_moderator_log()
	{
		if ($this->modlog)
		{
			require_once(DIR . '/includes/functions_log_error.php');

			$threadid = intval(($tid = $this->fetch_field('threadid')) ? $tid : $this->info['thread']['threadid']);
			$forumid = intval(($fid = $this->fetch_field('forumid')) ? $fid : $this->info['forum']['forumid']);

			if (can_moderate($forumid))
			{
				foreach ($this->modlog AS $entry)
				{
					$entry['forumid'] = $forumid;
					$entry['threadid'] = $threadid;
					log_moderator_action($entry, $entry['type'], $entry['action']);
				}
			}

			$this->modlog = array();
		}
	}

	function post_save_each($doquery = true)
	{
		$this->insert_moderator_log();

		if (!$this->condition AND $this->fetch_field('visible') == 1 AND $this->info['forum'])
		{
			$forumdata =& datamanager_init('Forum', $this->registry, ERRTYPE_SILENT);
			$forumdata->set_existing($this->info['forum']);
			$forumdata->set_info('disable_cache_rebuild', true);

			if (!empty($this->info['coventry']) AND $this->info['coventry']['in_coventry'] == 1)
			{
				$forumdata->set_info('coventry', $this->info['coventry']);
			}

			$forumdata->set('threadcount', 'threadcount + 1', false);

			$forumdata->save();
		}

		if ($this->condition AND $fpid = $this->fetch_field('firstpostid'))
		{
			if ($this->existing['visible'] == 0 AND $this->fetch_field('visible') == 1)
			{
				$this->akismet_mark_as_ham($fpid);
			}

			if (!$this->info['skip_first_post_update'])
			{
				// if we're updating the title/iconid of an existing thread, update the first post
				if ((isset($this->thread['title']) OR isset($this->thread['iconid'])) AND $fp = fetch_postinfo($fpid))
				{
					$postdata =& datamanager_init('Post', $this->registry, ERRTYPE_SILENT, 'threadpost');
					$postdata->set_existing($fp);

					if (isset($this->thread['title']))
					{
						$postdata->set('title', $this->thread['title'], true, false); // don't clean it -- already been cleaned
					}

					if (isset($this->thread['iconid']))
					{
						$postdata->set('iconid', $this->thread['iconid'], true, false);
					}

					$postdata->save();
				}
			}
		}

		if ($this->condition AND $this->thread['title'] AND $this->existing['title'])
		{
			// we're updating the title of a thread, so update redirect titles as well if the redirect title is the same
			$this->dbobject->query_write("
				UPDATE " . TABLE_PREFIX . "thread SET
					title = '" . $this->dbobject->escape_string($this->thread['title']) . "'
				WHERE
					open = 10 AND
					pollid = " . intval($this->fetch_field('threadid')) . " AND
					title = '" . $this->dbobject->escape_string($this->existing['title']) . "'
			");
		}

		if (!empty($this->info['coventry']) AND $this->info['coventry']['in_coventry'] == 1 AND $this->setfields['replycount'])
		{
			$this->dbobject->query_read("SELECT * FROM " . TABLE_PREFIX . "tachythreadcounter WHERE userid = " . $this->info['coventry']['userid'] . " AND threadid = " . $this->fetch_field('threadid'));
			if ($this->dbobject->affected_rows() > 0)
			{
				$tachyupdate = 'replycount = '. $this->tachythreadcounter['replycount'];
				$this->dbobject->query_write("
					UPDATE " . TABLE_PREFIX . "tachythreadcounter SET ". $tachyupdate . " WHERE userid = " . $this->info['coventry']['userid'] . " AND threadid = " . $this->fetch_field('threadid'));
			}
			else
			{
				$this->tachythreadcounter['replycount'] = 1;

				$this->tachythreadcounter['userid'] = $this->info['coventry']['userid'];
				$this->tachythreadcounter['threadid'] = $this->fetch_field('threadid');

				$this->dbobject->query_write("
					REPLACE INTO " . TABLE_PREFIX . "tachythreadcounter
						(userid, threadid, replycount)
					VALUES
						(" . intval($this->tachythreadcounter['userid']) . ",
						" . intval($this->tachythreadcounter['threadid']) . ",
						" . intval($this->tachythreadcounter['replycount']) . ")
				");
			}
		}

		if (empty($this->info['rebuild']) AND $this->setfields['lastpost'])
		{
			if (!empty($this->info['coventry']) AND $this->info['coventry']['in_coventry'] == 1)
			{
				$this->tachythreadpost['userid'] = $this->info['coventry']['userid'];
				$this->tachythreadpost['threadid'] = $this->fetch_field('threadid');


				$this->dbobject->query_write("
					REPLACE INTO " . TABLE_PREFIX . "tachythreadpost
						(userid, threadid, lastpost, lastposter, lastposterid, lastpostid)
					VALUES
						(" . intval($this->tachythreadpost['userid']) . ",
						" . intval($this->tachythreadpost['threadid']) . ",
						" . intval($this->tachythreadpost['lastpost']) . ",
						'" . $this->dbobject->escape_string($this->tachythreadpost['lastposter']) . "',
						" . intval($this->tachythreadpost['lastposterid']) . ",
						" . intval($this->tachythreadpost['lastpostid']) . ")
				");
			}
			else
			{
				$this->dbobject->query_write("
						DELETE FROM " . TABLE_PREFIX . "tachythreadpost
						WHERE threadid = " . intval($this->fetch_field('threadid'))
				);
			}
		}

		($hook = vBulletinHook::fetch_hook('threaddata_postsave')) ? eval($hook) : false;
	}

	/**
	* Deletes a thread
	*
	* @param	boolean	Whether to consider updating post counts, regardless of forum's settings
	* @param	boolean	Whether to physically remove the thread from the database
	* @param	array	Array of information for a soft delete
	* @param	boolean	Whether to add an entry to the moderator log
	*
	* @return	mixed	The number of affected rows
	*/
	function delete($countposts = true, $physicaldel = true, $delinfo = NULL, $dolog = true)
	{
		if ($threadid = $this->existing['threadid'])
		{
			require_once(DIR . '/includes/functions_databuild.php');
			require_once(DIR."/vb/search/core.php");

			($hook = vBulletinHook::fetch_hook('threaddata_delete')) ? eval($hook) : false;

			// Search index maintenance
			if ($physicaldel)
			{
				require_once(DIR . '/includes/class_taggablecontent.php');
				$content = vB_Taggable_Content_Item::create($vbulletin, "vBForum_Thread", $threadid);
				$content->delete_tag_attachments();

				//don't queue this, it needs to run before the thread records are deleted.
				$indexcontroller = vB_Search_Core::get_instance()->get_index_controller('vBForum', 'Post');
				$indexcontroller->delete_thread($threadid);
			}

			// note: the skip_moderator_log is the inverse of the $dolog argument
			return delete_thread($threadid, $countposts, $physicaldel, $delinfo, ($this->info['skip_moderator_log'] !== null ? !$this->info['skip_moderator_log'] : $dolog), $this->existing);
		}
		return false;
	}
}

/**
* Class to do data save/delete operations for a THREAD and its FIRST POST.
* This is an important distinction!
*
* @package	vBulletin
* @version	$Revision: 35848 $
* @date		$Date: 2010-03-17 12:38:45 -0700 (Wed, 17 Mar 2010) $
*/
class vB_DataManager_Thread_FirstPost extends vB_DataManager_Thread
{
	/**
	* Array of recognised and required fields for threads, and their types
	*
	* @var	array
	*/
	var $validfields = array(
		'firstpostid'   => array(TYPE_UINT, REQ_AUTO),
		'lastpost'      => array(TYPE_UINT, REQ_AUTO),
		'forumid'       => array(TYPE_UINT, REQ_YES),
		'pollid'        => array(TYPE_UINT, REQ_NO),
		'open'          => array(TYPE_UINT, REQ_AUTO,   VF_METHOD),
		'replycount'    => array(TYPE_UINT, REQ_AUTO),
		'hiddencount'   => array(TYPE_UINT, REQ_AUTO),
		'deletedcount'  => array(TYPE_UINT, REQ_AUTO),
		'lastposter'    => array(TYPE_STR,  REQ_AUTO),
		'lastposterid'  => array(TYPE_UINT, REQ_AUTO),
		'lastpostid'    => array(TYPE_UINT, REQ_AUTO),
		'views'         => array(TYPE_UINT, REQ_NO),
		'notes'         => array(TYPE_STR,  REQ_NO),
		'sticky'        => array(TYPE_UINT, REQ_NO,     VF_METHOD),
		'votenum'       => array(TYPE_UINT, REQ_NO),
		'votetotal'     => array(TYPE_UINT, REQ_NO),
		'similar'       => array(TYPE_STR,  REQ_AUTO),
		'prefixid'      => array(TYPE_STR,  REQ_NO,     VF_METHOD),
		'taglist'       => array(TYPE_STR,  REQ_NO),
		'keywords'      => array(TYPE_STR,  REQ_NO),

		// shared fields
		'threadid'      => array(TYPE_UINT, REQ_INCR),
		'title'         => array(TYPE_STR,  REQ_YES,    VF_METHOD),
		'username'      => array(TYPE_STR,  REQ_NO,     VF_METHOD), // maps to thread.postusername
		'userid'        => array(TYPE_UINT, REQ_NO,     VF_METHOD), // maps to thread.postuserid
		'dateline'      => array(TYPE_UINT, REQ_AUTO),
		'iconid'        => array(TYPE_UINT, REQ_NO,     VF_METHOD),
		'visible'       => array(TYPE_BOOL, REQ_NO), // note: post.visible will always be 1 with this object!
		'attach'        => array(TYPE_UINT, REQ_NO),

		// post only fields
		'pagetext'      => array(TYPE_STR,  REQ_YES,    VF_METHOD),
		'allowsmilie'   => array(TYPE_UINT, REQ_YES), // this is required as we must know whether smilies count as images
		'showsignature' => array(TYPE_BOOL, REQ_NO),
		'ipaddress'     => array(TYPE_STR,  REQ_AUTO),
		'htmlstate'     => array(TYPE_STR,  REQ_NO),
	);

	/**
	* Array of field names that are bitfields, together with the name of the variable in the registry with the definitions.
	*
	* @var	array
	*/
	var $bitfields = array();

	/**
	* The main table this class deals with
	*
	* @var	string
	*/
	var $table = 'thread';

	/**
	* Condition template for update query
	* This is for use with sprintf(). First key is the where clause, further keys are the field names of the data to be used.
	*
	* @var	array
	*/
	var $condition_construct = array('threadid = %1$d', 'threadid');

	/**
	* Array to store stuff to save to thread table
	*
	* @var	array
	*/
	var $thread = array();

	/**
	* Array to store stuff to save to post table
	*
	* @var	array
	*/
	var $post = array();

	/**
	* Constructor - checks that the registry object has been passed correctly.
	*
	* @param	vB_Registry	Instance of the vBulletin data registry object - expected to have the database object as one of its $this->db member.
	* @param	integer		One of the ERRTYPE_x constants
	*/
	function vB_DataManager_Thread_FirstPost(&$registry, $errtype = ERRTYPE_STANDARD)
	{
		parent::vB_DataManager($registry, $errtype);

		($hook = vBulletinHook::fetch_hook('threadfpdata_start')) ? eval($hook) : false;
	}

	/**
	* Takes valid data and sets it as part of the data to be saved
	*
	* @param	string	The name of the field to which the supplied data should be applied
	* @param	mixed	The data itself
	*/
	function do_set($fieldname, &$value)
	{
		$this->setfields["$fieldname"] = true;

		$tables = array();

		switch ($fieldname)
		{
			case 'threadid':
			case 'title':
			case 'dateline' :
			case 'iconid':
			case 'attach':
			{
				$tables = array('thread', 'post');
			}
			break;

			// post.visible will always be 1
			case 'visible':
			{
				$this->post['visible'] = 1;
				$this->thread['visible'] =& $value;
			}
			break;

			// exist in post table as is, but in the thread table as post<name>
			case 'username':
			case 'userid':
			{
				$this->post["$fieldname"] =& $value;
				$this->thread["post$fieldname"] =& $value;
				return;
			}
			break;

			case 'pagetext':
			case 'allowsmilie':
			case 'showsignature':
			case 'ipaddress':
			case 'htmlstate':
			{
				$tables = array('post');
			}
			break;

			default:
			{
				$tables = array('thread');
			}
		}

		($hook = vBulletinHook::fetch_hook('threadfpdata_doset')) ? eval($hook) : false;

		foreach ($tables AS $table)
		{
			$this->{$table}["$fieldname"] =& $value;
		}
	}

	/**
	* Saves thread data to the database
	*
	* @return	mixed
	*/
	function save($doquery = true)
	{
		if ($this->has_errors())
		{
			return false;
		}

		if (!$this->pre_save($doquery))
		{
			return 0;
		}

		if ($this->condition)
		{
			// update query
			$return = $this->db_update(TABLE_PREFIX, 'thread', $this->condition, $doquery);
			if ($return)
			{
				$this->db_update(TABLE_PREFIX, 'post', 'postid = ' . $this->fetch_field('firstpostid'), $doquery);
			}
		}
		else
		{
			// insert query
			$return = $this->thread['threadid'] = $this->db_insert(TABLE_PREFIX, 'thread', $doquery);

			if ($return)
			{
				$this->do_set('threadid', $return);

				$firstpostid = $this->thread['firstpostid'] = $this->db_insert(TABLE_PREFIX, 'post', $doquery);
				if ($doquery)
				{
					$this->dbobject->query_write("UPDATE " . TABLE_PREFIX . "thread SET firstpostid = $firstpostid, lastpostid = $firstpostid WHERE threadid = $return");
				}
			}
		}

		if ($return)
		{
			$this->post_save_each($doquery);
			$this->post_save_once($doquery);
		}

		return $return;
	}

	function pre_save($doquery = true)
	{
		if ($this->presave_called !== null)
		{
			return $this->presave_called;
		}

		if (!parent::pre_save($doquery))
		{
			$this->presave_called = false;
			return false;
		}

		if (!$this->pre_save_post($doquery))
		{
			$this->presave_called = false;
			return false;
		}

		if (!$this->condition)
		{
			$this->set('lastpost', $this->fetch_field('dateline'));
			$this->set('lastposter', $this->fetch_field('username', 'post'));
			$this->set('lastposterid', $this->fetch_field('userid', 'post'));
			$this->set('replycount', 0);
			$this->set('hiddencount', 0);
			$this->set('deletedcount', 0);
		}
		else
		{
			if (!$this->fetch_field('firstpostid'))
			{
				$getfirstpost = $this->dbobject->query_first("SELECT postid FROM " . TABLE_PREFIX . "post WHERE threadid = " . $this->fetch_field('threadid') . " ORDER BY dateline, postid LIMIT 1");
				$this->set('firstpostid', $getfirstpost['postid']);
			}
		}

		if (!$this->condition AND $this->fetch_field('open') === null)
		{
			$oldvalue = $this->info['skip_moderator_log'];
			$this->set_info('skip_moderator_log', true);
			$this->set('open', 1);
			$this->set_info('skip_moderator_log', $oldvalue);
		}

		$return_value = true;
		($hook = vBulletinHook::fetch_hook('threadfpdata_presave')) ? eval($hook) : false;

		// we've errored, so try to roll the floodcheck back if it happened
		if ($return_value == false AND !empty($this->floodcheck))
		{
			//$this->floodcheck->rollback();
		}

		$this->rebuild_keywords();

		$this->presave_called = $return_value;
		return $return_value;
	}
	//We need to index the changes
	//
	function post_save_once($doquery = true)
	{
		global $vbulletin;
		// do not index threads in the cms comment forum
		if ($this->fetch_field('forumid') == $vbulletin->options['vbcmsforumid'])
		{
			return;
		}
		
		static $saved = array();
		$threadid = $this->fetch_field('threadid');
		$postid = intval($this->fetch_field('firstpostid'));

		if (in_array($threadid, $saved))
		{
			return;
		}
		$saved[] = $threadid;

		require_once(DIR . '/vb/search/indexcontroller/queue.php');
		vb_Search_Indexcontroller_Queue::indexQueue('vBForum', 'Post', 'thread_data_change', $threadid);
		vb_Search_Indexcontroller_Queue::indexQueue('vBForum', 'Post', 'index', $postid);
	}

	function post_save_each($doquery = true)
	{
		if (!$this->condition AND $this->fetch_field('dateline') == TIMENOW)
		{
			$this->insert_dupehash(0);
		}

		$this->post_save_each_post($doquery);

		if (!$this->condition AND $this->fetch_field('dateline') == TIMENOW)
		{
			$this->insert_postlog_data();
		}

		$threadid = intval($this->fetch_field('threadid'));

		if ($this->thread['visible'] === 0)
		{
			$postid = intval($this->fetch_field('firstpostid'));

			/*insert query*/
			$this->dbobject->query_write("INSERT IGNORE INTO " . TABLE_PREFIX . "moderation (primaryid, type, dateline) VALUES ($threadid, 'thread', " . TIMENOW . ")");
		}

		if ($this->info['forum']['podcast'] AND $postid = intval($this->fetch_field('firstpostid')))
		{
			$this->dbobject->query_write("
				REPLACE INTO " . TABLE_PREFIX . "podcastitem
					(postid, url, length, explicit, author, keywords, subtitle)
				VALUES
					(
						$postid,
						'" . $this->dbobject->escape_string($this->info['podcasturl']) . "',
						" . intval($this->info['podcastsize']) . ",
						" . intval($this->info['podcastexplicit']) . ",
						'" . $this->dbobject->escape_string($this->info['podcastauthor']) . "',
						'" . $this->dbobject->escape_string($this->info['podcastkeywords']) . "',
						'" . $this->dbobject->escape_string($this->info['podcastsubtitle']) . "'
					)
			");

			// reset rss cache for this forum
			$this->dbobject->query_write("
				DELETE FROM " . TABLE_PREFIX . "externalcache
				WHERE forumid = " . intval($this->info['forum']['forumid']) . "
			");
		}

		if ($this->info['mark_thread_read'] AND $this->info['forum'] AND $this->registry->options['threadmarking'] AND $userid = $this->fetch_field('postuserid'))
		{
			$threadinfo = fetch_threadinfo($threadid);
			if ($threadinfo)
			{
				require_once(DIR . '/includes/functions_bigthree.php');
				mark_thread_read($threadinfo, $this->info['forum'], $userid, $this->fetch_field('dateline'));
			}
		}

		$this->insert_moderator_log();

		if (!$this->condition)
		{
			$this->email_moderators(array('newthreademail', 'newpostemail'));
		}

		if ($this->info['forum'] AND $this->fetch_field('firstpostid'))
		{
			// ### UPDATE SEARCH INDEX ###
			require_once(DIR . '/includes/functions_databuild.php');
			build_post_index($this->fetch_field('firstpostid'), $this->info['forum'], 1);
		}

		($hook = vBulletinHook::fetch_hook('threadfpdata_postsave')) ? eval($hook) : false;
	}

	/**
	* Deletes a thread with the first post
	*
	* @param	boolean	Whether to consider updating post counts, regardless of forum's settings
	* @param	boolean	Whether to physically remove the thread from the database
	* @param	array	Array of information for a soft delete
	* @param	boolean	Whether to add an entry to the moderator log
	*
	* @return	mixed	The number of affected rows
	*/
	function delete($countposts = true, $physicaldel = true, $delinfo = NULL, $dolog = true)
	{
 		require_once(DIR . '/vb/search/core.php');
      	// TODO: follow up on and check $this->existing['threadid']

		if ($threadid = $this->existing['threadid'])
		{
			// Search index maintenance
			if ($physicaldel)
			{
				require_once(DIR . '/vb/search/indexcontroller/queue.php');
				vb_Search_Indexcontroller_Queue::indexQueue('vBForum', 'Post', 'delete', $threadid);

				require_once(DIR . '/includes/class_taggablecontent.php');
				$content = vB_Taggable_Content_Item::create($vbulletin, "vBForum_Thread", $threadid);
				$content->delete_tag_attachments();
			}
		}

		($hook = vBulletinHook::fetch_hook('threadfpdata_delete')) ? eval($hook) : false;

		return parent::delete($countposts, $physicaldel, $delinfo, ($this->info['skip_moderator_log'] !== null ? !$this->info['skip_moderator_log'] : $dolog));
	}
}

/**
* Class to do data update operations for multiple POSTS simultaneously
*
* @package	vBulletin
* @version	$Revision: 35848 $
* @date		$Date: 2010-03-17 12:38:45 -0700 (Wed, 17 Mar 2010) $
*/
class vB_DataManager_Post_Multiple extends vB_DataManager_Multiple
{
	/**
	* The name of the class to instantiate for each matching. It is assumed to exist!
	* It should be a subclass of vB_DataManager.
	*
	* @var	string
	*/
	var $class_name = 'vB_DataManager_Post';

	/**
	* The name of the primary ID column that is used to uniquely identify records retrieved.
	* This will be used to build the condition in all update queries!
	*
	* @var string
	*/
	var $primary_id = 'postid';

	/**
	* Builds the SQL to run to fetch records. This must be overridden by a child class!
	*
	* @param	string	Condition to use in the fetch query; the entire WHERE clause
	* @param	integer	The number of records to limit the results to; 0 is unlimited
	* @param	integer	The number of records to skip before retrieving matches.
	*
	* @return	string	The query to execute
	*/
	function fetch_query($condition, $limit = 0, $offset = 0)
	{
		$query = "SELECT * FROM " . TABLE_PREFIX . "post AS post";

		if ($condition)
		{
			$query .= " WHERE $condition";
		}

		$limit = intval($limit);
		$offset = intval($offset);
		if ($limit)
		{
			$query .= " LIMIT $offset, $limit";
		}

		return $query;
	}
}

/**
* Class to do data update operations for multiple THREADS simultaneously
*
* @package	vBulletin
* @version	$Revision: 35848 $
* @date		$Date: 2010-03-17 12:38:45 -0700 (Wed, 17 Mar 2010) $
*/
class vB_DataManager_Thread_Multiple extends vB_DataManager_Multiple
{
	/**
	* The name of the class to instantiate for each matching. It is assumed to exist!
	* It should be a subclass of vB_DataManager.
	*
	* @var	string
	*/
	var $class_name = 'vB_DataManager_Thread';

	/**
	* The name of the primary ID column that is used to uniquely identify records retrieved.
	* This will be used to build the condition in all update queries!
	*
	* @var string
	*/
	var $primary_id = 'threadid';

	/**
	* Builds the SQL to run to fetch records. This must be overridden by a child class!
	*
	* @param	string	Condition to use in the fetch query; the entire WHERE clause
	* @param	integer	The number of records to limit the results to; 0 is unlimited
	* @param	integer	The number of records to skip before retrieving matches.
	*
	* @return	string	The query to execute
	*/
	function fetch_query($condition, $limit = 0, $offset = 0)
	{
		$query = "SELECT * FROM " . TABLE_PREFIX . "thread AS thread";
		if ($condition)
		{
			$query .= " WHERE $condition";
		}

		$limit = intval($limit);
		$offset = intval($offset);
		if ($limit)
		{
			$query .= " LIMIT $offset, $limit";
		}

		return $query;
	}
}

/*======================================================================*\
|| ####################################################################
|| # CVS: $RCSfile$ - $Revision: 35848 $
|| ####################################################################
\*======================================================================*/