<?php

/**
 * Model for profile post related functions.
 *
 * @package XenForo_ProfilePost
 */
class XenForo_Model_ProfilePost extends XenForo_Model
{
	/**
	 * Constants to allow joins to extra tables in certain queries
	 *
	 * @var integer Join user table (poster)
	 * @var integer Join user table (receiver)
	 * @var integer Join user privacy table (receiver)
	 * @var integer Join deletion log
	 */
	const FETCH_USER_POSTER = 0x01;
	const FETCH_USER_RECEIVER = 0x02;
	const FETCH_USER_RECEIVER_PRIVACY = 0x04;
	const FETCH_DELETION_LOG = 0x08;

	/**
	 * Gets the specified profile post.
	 *
	 * @param integer $id
	 * @param array $fetchOptions
	 *
	 * @return array|false
	 */
	public function getProfilePostById($id, array $fetchOptions = array())
	{
		$sqlClauses = $this->prepareProfilePostFetchOptions($fetchOptions);

		return $this->_getDb()->fetchRow('
			SELECT profile_post.*
				' . $sqlClauses['selectFields'] . '
			FROM xf_profile_post AS profile_post
			' . $sqlClauses['joinTables'] . '
			WHERE profile_post.profile_post_id = ?
		', $id);
	}

	/**
	 * Gets profile posts for the specified user that meet the given conditions.
	 *
	 * @param integer $userId
	 * @param array $conditions
	 * @param array $fetchOptions
	 *
	 * @return array [profile post id] => info
	 */
	public function getProfilePostsForUserId($userId, array $conditions = array(), array $fetchOptions = array())
	{
		$conditions['user_id'] = $userId;
		$whereClause = $this->prepareProfilePostConditions($conditions, $fetchOptions);

		$sqlClauses = $this->prepareProfilePostFetchOptions($fetchOptions);
		$limitOptions = $this->prepareLimitFetchOptions($fetchOptions);

		return $this->fetchAllKeyed($this->limitQueryResults(
			'
				SELECT profile_post.*
					' . $sqlClauses['selectFields'] . '
				FROM xf_profile_post AS profile_post
				' . $sqlClauses['joinTables'] . '
				WHERE ' . $whereClause . '
				ORDER BY profile_post.post_date DESC
			', $limitOptions['limit'], $limitOptions['offset']
		), 'profile_post_id');
	}

	/**
	 * Counts the profile posts that were left for the specified user (with the given conditions).
	 *
	 * @param integer $userId
	 * @param array $conditions
	 *
	 * @return integer
	 */
	public function countProfilePostsForUserId($userId, array $conditions = array())
	{
		$fetchOptions = array();
		$conditions['user_id'] = $userId;
		$whereClause = $this->prepareProfilePostConditions($conditions, $fetchOptions);

		$sqlClauses = $this->prepareProfilePostFetchOptions($fetchOptions);

		return $this->_getDb()->fetchOne('
			SELECT COUNT(*)
			FROM xf_profile_post AS profile_post
			' . $sqlClauses['joinTables'] . '
			WHERE ' . $whereClause
		);
	}

	/**
	 * Gets the specified profile posts.
	 *
	 * @param array $messageIds
	 * @param array $fetchOptions
	 *
	 * @return array [profile post id] => info
	 */
	public function getProfilePostsByIds(array $messageIds, array $fetchOptions = array())
	{
		if (!$messageIds)
		{
			return array();
		}

		$sqlClauses = $this->prepareProfilePostFetchOptions($fetchOptions);

		return $this->fetchAllKeyed('
			SELECT profile_post.*
				' . $sqlClauses['selectFields'] . '
			FROM xf_profile_post AS profile_post
			' . $sqlClauses['joinTables'] . '
			WHERE profile_post.profile_post_id IN(' . $this->_getDb()->quote($messageIds) . ')
		', 'profile_post_id');
	}

	/**
	 * Fetches all profile posts posted by the specified user, on their own and others' profile pages.
	 *
	 * @param integer $userId
	 * @param array $fetchOptions
	 *
	 * @return array
	 */
	public function getProfilePostsByUserId($userId, array $fetchOptions = array())
	{
		$sqlClauses = $this->prepareProfilePostFetchOptions($fetchOptions);

		return $this->fetchAllKeyed('
			SELECT profile_post.*
				' . $sqlClauses['selectFields'] . '
			FROM xf_profile_post AS profile_post
			' . $sqlClauses['joinTables'] . '
			WHERE profile_post.user_id = ?
		', 'profile_post_id', $userId);
	}

	/**
	 * Gets profile post IDs in the specified range. The IDs returned will be those immediately
	 * after the "start" value (not including the start), up to the specified limit.
	 *
	 * @param integer $start IDs greater than this will be returned
	 * @param integer $limit Number of posts to return
	 *
	 * @return array List of IDs
	 */
	public function getProfilePostIdsInRange($start, $limit)
	{
		$db = $this->_getDb();

		return $db->fetchCol($db->limit('
			SELECT profile_post_id
			FROM xf_profile_post
			WHERE profile_post_id > ?
			ORDER BY profile_post_id
		', $limit), $start);
	}

	/**
	 * Gets the latest profile posts.
	 *
	 * @param array $conditions
	 * @param array $fetchOptions
	 *
	 * @return array [profile post id] => info
	 */
	public function getLatestProfilePosts(array $conditions = array(), array $fetchOptions = array())
	{
		$whereClause = $this->prepareProfilePostConditions($conditions, $fetchOptions);

		$sqlClauses = $this->prepareProfilePostFetchOptions($fetchOptions);
		$limitOptions = $this->prepareLimitFetchOptions($fetchOptions);

		return $this->fetchAllKeyed($this->limitQueryResults(
			'
				SELECT profile_post.*
					' . $sqlClauses['selectFields'] . '
				FROM xf_profile_post AS profile_post
				' . $sqlClauses['joinTables'] . '
				WHERE ' . $whereClause . '
				ORDER BY profile_post.post_date DESC
			', $limitOptions['limit'], $limitOptions['offset']
		), 'profile_post_id');
	}

	/**
	 * Prepares a collection of profile post fetching related conditions into an SQL clause
	 *
	 * @param array $conditions List of conditions
	 * @param array $fetchOptions Modifiable set of fetch options (may have joins pushed on to it)
	 *
	 * @return string SQL clause (at least 1=1)
	 */
	public function prepareProfilePostConditions(array $conditions, array &$fetchOptions)
	{
		$sqlConditions = array();
		$db = $this->_getDb();

		if (!empty($conditions['user_id']))
		{
			$sqlConditions[] = "profile_post.profile_user_id = " . $db->quote($conditions['user_id']);
		}

		if (!empty($conditions['post_date']) && is_array($conditions['post_date']))
		{
			list($operator, $cutOff) = $conditions['post_date'];

			$this->assertValidCutOffOperator($operator);
			$sqlConditions[] = "profile_post.post_date $operator " . $db->quote($cutOff);
		}

		if (isset($conditions['deleted']) || isset($conditions['moderated']))
		{
			$sqlConditions[] = $this->prepareStateLimitFromConditions($conditions, 'profile_post', 'message_state');
		}

		return $this->getConditionsForClause($sqlConditions);
	}

	/**
	 * Checks the 'join' key of the incoming array for the presence of the FETCH_x bitfields in this class
	 * and returns SQL snippets to join the specified tables if required
	 *
	 * @param array $fetchOptions containing a 'join' integer key build from this class's FETCH_x bitfields
	 *
	 * @return array Containing 'selectFields' and 'joinTables' keys. Example: selectFields = ', user.*, foo.title'; joinTables = ' INNER JOIN foo ON (foo.id = other.id) '
	 */
	public function prepareProfilePostFetchOptions(array $fetchOptions)
	{
		$selectFields = '';
		$joinTables = '';

		$db = $this->_getDb();

		if (!empty($fetchOptions['join']))
		{
			if ($fetchOptions['join'] & self::FETCH_USER_POSTER)
			{
				$selectFields .= ',
					posting_user.*,
					IF(posting_user.username IS NULL, profile_post.username, posting_user.username) AS username';
				$joinTables .= '
					LEFT JOIN xf_user AS posting_user ON
						(posting_user.user_id = profile_post.user_id)';
			}

			if ($fetchOptions['join'] & self::FETCH_USER_RECEIVER)
			{
				$selectFields .= ',
					receiving_user.username
						AS profile_username,
					receiving_user.gender
						AS profile_gender,
					receiving_user.user_state
						AS profile_user_state,
					receiving_user.user_group_id
						AS profile_user_group_id,
					receiving_user.secondary_group_ids
						AS profile_secondary_group_ids,
					receiving_user.display_style_group_id
						AS profile_display_style_group_id,
					receiving_user.is_banned
						AS profile_is_banned,
					receiving_user.is_admin
						AS profile_is_admin,
					receiving_user.is_moderator
						AS profile_is_moderator,
					receiving_user.avatar_date
						AS profile_avatar_date,
					receiving_user.gravatar
						AS profile_gravatar,
					receiving_user.warning_points
						AS profile_warning_points,
					receiving_user.permission_combination_id
						AS profile_permission_combination_id';
				$joinTables .= '
					LEFT JOIN xf_user AS receiving_user ON
						(receiving_user.user_id = profile_post.profile_user_id)';
			}

			if ($fetchOptions['join'] & self::FETCH_USER_RECEIVER_PRIVACY)
			{
				$visitingUser = (isset($fetchOptions['visitingUser']) ? $fetchOptions['visitingUser'] : XenForo_Visitor::getInstance());

				$selectFields .= ',
					receiving_user_privacy.allow_view_profile
						AS profile_allow_view_profile,
					receiving_user_privacy.allow_post_profile
						AS profile_allow_post_profile,
					receiving_user_privacy.allow_send_personal_conversation
						AS profile_allow_send_personal_conversation,
					receiving_user_privacy.allow_view_identities
						AS profile_allow_view_identities,
					receiving_user_privacy.allow_receive_news_feed
						AS profile_allow_allow_receive_news_feed,
					IF (receiving_user_follow.follow_user_id, 1, 0)
						AS following_' . intval($visitingUser['user_id']);
				$joinTables .= '
					LEFT JOIN xf_user_privacy AS receiving_user_privacy ON
						(receiving_user_privacy.user_id = profile_post.profile_user_id)
					LEFT JOIN xf_user_follow AS receiving_user_follow ON
						(receiving_user_follow.user_id = profile_post.profile_user_id AND follow_user_id = ' . $db->quote($visitingUser['user_id']) . ')';
			}

			if ($fetchOptions['join'] & self::FETCH_DELETION_LOG)
			{
				$selectFields .= ',
					deletion_log.delete_date, deletion_log.delete_reason,
					deletion_log.delete_user_id, deletion_log.delete_username';
				$joinTables .= '
					LEFT JOIN xf_deletion_log AS deletion_log ON
						(deletion_log.content_type = \'profile_post\' AND deletion_log.content_id = profile_post.profile_post_id)';
			}
		}

		if (isset($fetchOptions['likeUserId']))
		{
			if (empty($fetchOptions['likeUserId']))
			{
				$selectFields .= ',
					0 AS like_date';
			}
			else
			{
				$selectFields .= ',
					liked_content.like_date';
				$joinTables .= '
					LEFT JOIN xf_liked_content AS liked_content
						ON (liked_content.content_type = \'profile_post\'
							AND liked_content.content_id = profile_post.profile_post_id
							AND liked_content.like_user_id = ' .$db->quote($fetchOptions['likeUserId']) . ')';
			}
		}

		return array(
			'selectFields' => $selectFields,
			'joinTables'   => $joinTables
		);
	}

	/**
	 * Prepares a profile post.
	 *
	 * @param array $profilePost
	 * @param array $user User whose profile the post is on
	 * @param array|null $viewingUser Viewing user reference
	 *
	 * @return array
	 */
	public function prepareProfilePost(array $profilePost, array $user, array $viewingUser = null)
	{
		$this->standardizeViewingUserReference($viewingUser);

		$profilePost['canDelete'] = $this->canDeleteProfilePost($profilePost, $user, 'soft', $null, $viewingUser);
		$profilePost['canEdit'] = $this->canEditProfilePost($profilePost, $user, $null, $viewingUser);
		$profilePost['canLike'] = $this->canLikeProfilePost($profilePost, $user, $null, $viewingUser);
		$profilePost['canComment'] = $this->canCommentOnProfilePost($profilePost, $user, $null, $viewingUser);
		$profilePost['canReport'] = $this->canReportProfilePost($profilePost, $user, $null, $viewingUser);
		$profilePost['canWarn'] = $this->canWarnProfilePost($profilePost, $user, $null, $viewingUser);

		$profilePost['isDeleted'] = ($profilePost['message_state'] == 'deleted');
		$profilePost['isModerated'] = ($profilePost['message_state'] == 'moderated');

		$profilePost['canCleanSpam'] = (
			$profilePost['canDelete']
			&& $user['user_id']
			&& XenForo_Permission::hasPermission($viewingUser['permissions'], 'general', 'cleanSpam')
			&& $this->_getUserModel()->couldBeSpammer($profilePost)
		);

		if (!empty($profilePost['delete_date']))
		{
			$profilePost['deleteInfo'] = array(
				'user_id' => $profilePost['delete_user_id'],
				'username' => $profilePost['delete_username'],
				'date' => $profilePost['delete_date'],
				'reason' => $profilePost['delete_reason'],
			);
		}

		if (!empty($profilePost['profile_username']))
		{
			$profilePost['profileUser'] = $this->getProfileUserFromProfilePost($profilePost);
		}

		if ($profilePost['likes'])
		{
			$profilePost['likeUsers'] = unserialize($profilePost['like_users']);
		}

		if (array_key_exists('user_group_id', $profilePost))
		{
			$profilePost = $this->getModelFromCache('XenForo_Model_User')->prepareUser($profilePost);
		}

		return $profilePost;
	}

	/**
	 * Prepares a batch of profile posts.
	 *
	 * @param array $profilePosts
	 * @param array $user User whose profile the post is on
	 * @param array|null $viewingUser Viewing user reference
	 *
	 * @return array
	 */
	public function prepareProfilePosts(array $profilePosts, array $user, array $viewingUser = null)
	{
		$this->standardizeViewingUserReference($viewingUser);

		foreach ($profilePosts AS &$profilePost)
		{
			$profilePost = $this->prepareProfilePost($profilePost, $user, $viewingUser);
		}

		return $profilePosts;
	}

	/**
	 * Extracts the data relating to a profile owner user from a profile post,
	 * that has been fetched with XenForo_Model_ProfilePost::FETCH_USER_RECEIVER
	 *
	 * @param array $profilePost
	 *
	 * @return array
	 */
	public function getProfileUserFromProfilePost(array $profilePost, array $viewingUser = null)
	{
		$this->standardizeViewingUserReference($viewingUser);

		$user = array();

		$followingKey = 'following_' . $viewingUser['user_id'];
		if (isset($profilePost[$followingKey]))
		{
			$user[$followingKey] = $profilePost[$followingKey];
		}

		foreach ($profilePost AS $key => $val)
		{
			if (preg_match('/^profile_(.+)$/', $key, $match))
			{
				$user[$match[1]] = $val;
			}
		}

		return $user;
	}

	/**
	 * Determines if the given profile post can be viewed. This does not
	 * check user profile viewing permissions.
	 *
	 * @param array $profilePost
	 * @param array $user
	 * @param string $errorPhraseKey By ref. More specific error, if available.
	 * @param array|null $viewingUser Viewing user reference
	 *
	 * @return boolean
	 */
	public function canViewProfilePost(array $profilePost, array $user, &$errorPhraseKey = '', array $viewingUser = null)
	{
		$this->standardizeViewingUserReference($viewingUser);

		if (!XenForo_Permission::hasPermission($viewingUser['permissions'], 'profilePost', 'view'))
		{
			return false;
		}

		if ($profilePost['message_state'] == 'moderated'
			&& !XenForo_Permission::hasPermission($viewingUser['permissions'], 'profilePost', 'viewModerated')
		)
		{
			if (!$viewingUser['user_id'] || $viewingUser['user_id'] != $profilePost['user_id'])
			{
				return false;
			}
		}
		else if ($profilePost['message_state'] == 'deleted'
			&& !XenForo_Permission::hasPermission($viewingUser['permissions'], 'profilePost', 'viewDeleted')
		)
		{
			return false;
		}

		return $this->_getUserProfileModel()->canViewProfilePosts($user, $errorPhraseKey, $viewingUser);
	}

	/**
	 * Determines if the given profile post can be viewed. This checks parent permissions.
	 *
	 * @param array $profilePost
	 * @param array $user
	 * @param string $errorPhraseKey By ref. More specific error, if available.
	 * @param array|null $viewingUser Viewing user reference
	 *
	 * @return boolean
	 */
	public function canViewProfilePostAndContainer(array $profilePost, array $user, &$errorPhraseKey = '', array $viewingUser = null)
	{
		$this->standardizeViewingUserReference($viewingUser);

		if ($user['username'] === null)
		{
			// likely represents a failed join
			return false;
		}

		if (!$this->_getUserProfileModel()->canViewFullUserProfile($user, $errorPhraseKey, $viewingUser))
		{
			return false;
		}

		return $this->canViewProfilePost($profilePost, $user, $errorPhraseKey, $viewingUser);
	}

	/**
	 * Determines if the given profile post can be edited. This does not
	 * check parent (viewing) permissions.
	 *
	 * @param array $profilePost
	 * @param array $user
	 * @param string $errorPhraseKey By ref. More specific error, if available.
	 * @param array|null $viewingUser Viewing user reference
	 *
	 * @return boolean
	 */
	public function canEditProfilePost(array $profilePost, array $user, &$errorPhraseKey = '', array $viewingUser = null)
	{
		$this->standardizeViewingUserReference($viewingUser);

		if (!$viewingUser['user_id'])
		{
			return false;
		}

		if ($viewingUser['user_id'] == $profilePost['user_id'])
		{
			return XenForo_Permission::hasPermission($viewingUser['permissions'], 'profilePost', 'editOwn');
		}
		else
		{
			return XenForo_Permission::hasPermission($viewingUser['permissions'], 'profilePost', 'editAny');
		}
	}

	/**
	 * Determines if the given profile post can be deleted. This does not
	 * check parent (viewing) permissions.
	 *
	 * @param array $profilePost
	 * @param array $user
	 * @param string $deleteType Type of deletion (soft or hard)
	 * @param string $errorPhraseKey By ref. More specific error, if available.
	 * @param array|null $viewingUser Viewing user reference
	 *
	 * @return boolean
	 */
	public function canDeleteProfilePost(array $profilePost, array $user, $deleteType = 'soft', &$errorPhraseKey = '', array $viewingUser = null)
	{
		$this->standardizeViewingUserReference($viewingUser);

		if (!$viewingUser['user_id'])
		{
			return false;
		}

		if ($deleteType != 'soft' && !XenForo_Permission::hasPermission($viewingUser['permissions'], 'profilePost', 'hardDeleteAny'))
		{
			return false;
		}

		if (XenForo_Permission::hasPermission($viewingUser['permissions'], 'profilePost', 'deleteAny'))
		{
			return true;
		}

		return (
			(
				$viewingUser['user_id'] == $profilePost['profile_user_id']
				&& XenForo_Permission::hasPermission($viewingUser['permissions'], 'profilePost', 'manageOwn')
			)
			||
			(
				$viewingUser['user_id'] == $profilePost['user_id']
				&& XenForo_Permission::hasPermission($viewingUser['permissions'], 'profilePost', 'deleteOwn')
			)
		);
	}

	/**
	 * Determines if the given profile post can be undeleted. This does not
	 * check parent (viewing) permissions.
	 *
	 * @param array $profilePost
	 * @param array $user
	 * @param string $errorPhraseKey By ref. More specific error, if available.
	 * @param array|null $viewingUser Viewing user reference
	 *
	 * @return boolean
	 */
	public function canUndeleteProfilePost(array $profilePost, array $user, &$errorPhraseKey = '', array $viewingUser = null)
	{
		$this->standardizeViewingUserReference($viewingUser);
		return ($viewingUser['user_id'] && XenForo_Permission::hasPermission($viewingUser['permissions'], 'profilePost', 'undelete'));
	}

	/**
	 * Determines if an action alert can be sent for this profile post. This does not
	 * check parent (viewing) permissions.
	 *
	 * @param array $profilePost
	 * @param array $user
	 * @param string $errorPhraseKey By ref. More specific error, if available.
	 * @param array|null $viewingUser Viewing user reference
	 *
	 * @return boolean
	 */
	public function canSendProfilePostActionAlert(array $profilePost, array $user, &$errorPhraseKey = '', array $viewingUser = null)
	{
		$this->standardizeViewingUserReference($viewingUser);

		if (!$viewingUser['user_id'] || $viewingUser['user_id'] == $profilePost['user_id'])
		{
			return false;
		}

		if ($profilePost['message_state'] != 'visible')
		{
			return false;
		}

		return (
			XenForo_Permission::hasPermission($viewingUser['permissions'], 'profilePost', 'deleteAny')
			|| XenForo_Permission::hasPermission($viewingUser['permissions'], 'profilePost', 'editAny')
		);
	}

	/**
	 * Determines if the given profile post can be approved/unapproved. This does not
	 * check parent (viewing) permissions.
	 *
	 * @param array $profilePost
	 * @param array $user
	 * @param string $errorPhraseKey By ref. More specific error, if available.
	 * @param array|null $viewingUser Viewing user reference
	 *
	 * @return boolean
	 */
	public function canApproveUnapproveProfilePost(array $profilePost, array $user, &$errorPhraseKey = '', array $viewingUser = null)
	{
		$this->standardizeViewingUserReference($viewingUser);
		return ($viewingUser['user_id'] && XenForo_Permission::hasPermission($viewingUser['permissions'], 'profilePost', 'approveUnapprove'));
	}

	/**
	 * Determines if the given profile post can be warned. This does not
	 * check parent (viewing) permissions.
	 *
	 * @param array $profilePost
	 * @param array $user
	 * @param string $errorPhraseKey By ref. More specific error, if available.
	 * @param array|null $viewingUser Viewing user reference
	 *
	 * @return boolean
	 */
	public function canWarnProfilePost(array $profilePost, array $user, &$errorPhraseKey = '', array $viewingUser = null)
	{
		if ($profilePost['warning_id'] || empty($profilePost['user_id']))
		{
			return false;
		}

		if (!empty($profilePost['is_admin']) || !empty($profilePost['is_moderator']))
		{
			return false;
		}

		$this->standardizeViewingUserReference($viewingUser);

		return ($viewingUser['user_id'] && XenForo_Permission::hasPermission($viewingUser['permissions'], 'profilePost', 'warn'));
	}

	/**
	 * Determines if the profile post can be liked with the given permissions.
	 * This does not check profile post viewing permissions.
	 *
	 * @param array $profilePost Profile post info
	 * @param array $user User info for where profile post is posted
	 * @param string $errorPhraseKey Returned phrase key for a specific error
	 * @param array|null $viewingUser
	 *
	 * @return boolean
	 */
	public function canLikeProfilePost(array $profilePost, array $user, &$errorPhraseKey = '', array $viewingUser = null)
	{
		$this->standardizeViewingUserReference($viewingUser);

		if (!$viewingUser['user_id'])
		{
			return false;
		}

		if ($profilePost['message_state'] != 'visible')
		{
			return false;
		}

		if ($profilePost['user_id'] == $viewingUser['user_id'])
		{
			$errorPhraseKey = 'liking_own_content_cheating';
			return false;
		}

		return XenForo_Permission::hasPermission($viewingUser['permissions'], 'profilePost', 'like');
	}

	/**
	 * Determines if the given profile post can be commented on. This does not
	 * check parent (viewing) permissions.
	 *
	 * @param array $profilePost
	 * @param array $user
	 * @param string $errorPhraseKey By ref. More specific error, if available.
	 * @param array|null $viewingUser Viewing user reference
	 *
	 * @return boolean
	 */
	public function canCommentOnProfilePost(array $profilePost, array $user, &$errorPhraseKey = '', array $viewingUser = null)
	{
		$this->standardizeViewingUserReference($viewingUser);

		if (!$viewingUser['user_id'])
		{
			return false;
		}

		if ($profilePost['message_state'] != 'visible')
		{
			return false;
		}

		return (
			XenForo_Permission::hasPermission($viewingUser['permissions'], 'profilePost', 'view')
			&& XenForo_Permission::hasPermission($viewingUser['permissions'], 'profilePost', 'comment')
			&& $this->_getUserModel()->passesPrivacyCheck($user['allow_post_profile'], $user, $viewingUser)
		);
	}

	/**
	 * Determines if the viewing user can view the IP of the stated profile post
	 *
	 * @param array $profilePost
	 * @param array $user
	 * @param string $errorPhraseKey By ref. More specific error, if available.
	 * @param array|null $viewingUser Viewing user reference
	 *
	 * @return boolean
	 */
	public function canViewIps(array $profilePost, array $user, &$errorPhraseKey = '', array $viewingUser = null)
	{
		return $this->getModelFromCache('XenForo_Model_User')->canViewIps($errorPhraseKey, $viewingUser);
	}

	/**
	 * Checks that the viewing user may report the specified profile post
	 *
	 * @param array $profilePost
	 * @param array $user
	 * @param string
	 * @param boolean $errorPhraseKey
	 * @param array|null $viewingUser
	 *
	 * @return boolean
	 */
	public function canReportProfilePost(array $profilePost, array $user, &$errorPhraseKey = '', array $viewingUser = null)
	{
		return $this->getModelFromCache('XenForo_Model_User')->canReportContent($errorPhraseKey, $viewingUser);
	}

	/**
	 * Adds the inline mod option to a profile post.
	 *
	 * @param array $profilePost Profile post. By reference; canInlineMod key added
	 * @param array $user
	 * @param array|null $viewingUser
	 *
	 * @return array List of inline mod options the user can do
	 */
	public function addInlineModOptionToProfilePost(array &$profilePost, array $user, array $viewingUser = null)
	{
		$this->standardizeViewingUserReference($viewingUser);

		$options = array();
		$canInlineMod = ($viewingUser['user_id'] && (
			XenForo_Permission::hasPermission($viewingUser['permissions'], 'profilePost', 'deleteAny')
			|| XenForo_Permission::hasPermission($viewingUser['permissions'], 'profilePost', 'undelete')
			|| XenForo_Permission::hasPermission($viewingUser['permissions'], 'profilePost', 'approveUnapprove')
		));

		if ($canInlineMod)
		{
			if ($this->canDeleteProfilePost($profilePost, $user, 'soft', $null, $viewingUser))
			{
				$options['delete'] = true;
			}
			if ($this->canUndeleteProfilePost($profilePost, $user, $null, $viewingUser))
			{
				$options['undelete'] = true;
			}
			if ($this->canApproveUnapproveProfilePost($profilePost, $user, $null, $viewingUser))
			{
				$options['approve'] = true;
				$options['unapprove'] = true;
			}
		}

		$profilePost['canInlineMod'] = (count($options) > 0);

		return $options;
	}

	/**
	 * Adds the inline mod option to a batch of profile posts.
	 *
	 * @param array $profilePosts Batch of profile posts. By reference; all modified to add canInlineMod key
	 * @param array $user
	 * @param array|null $viewingUser
	 *
	 * @return array List of inline mod options the user can do on at least one post
	 */
	public function addInlineModOptionToProfilePosts(array &$profilePosts, array $user, array $viewingUser = null)
	{
		$this->standardizeViewingUserReference($viewingUser);

		$options = array();
		foreach ($profilePosts AS &$profilePost)
		{
			$options += $this->addInlineModOptionToProfilePost($profilePost, $user, $viewingUser);
		}

		return $options;
	}

	/**
	 * Gets permission-based conditions that apply to profile post fetching functions.
	 *
	 * @param array $user User the profile posts will belong to
	 * @param array|null $viewingUser Viewing user ref; defaults to visitor
	 *
	 * @return array Keys: deleted (boolean), moderated (boolean, integer if can only view specific user ID)
	 */
	public function getPermissionBasedProfilePostConditions(array $user, array $viewingUser = null)
	{
		$this->standardizeViewingUserReference($viewingUser);

		if (XenForo_Permission::hasPermission($viewingUser['permissions'], 'profilePost', 'viewModerated'))
		{
			$viewModerated = true;
		}
		else if ($viewingUser['user_id'])
		{
			$viewModerated = $viewingUser['user_id'];
		}
		else
		{
			$viewModerated = false;
		}

		return array(
			'deleted' => XenForo_Permission::hasPermission($viewingUser['permissions'], 'profilePost', 'viewDeleted'),
			'moderated' => $viewModerated
		);
	}

	/**
	 * Gets the message state for a newly inserted profile post by the viewing user.
	 *
	 * @param array $user User whose profile is being posted on
	 * @param array|null $viewingUser
	 *
	 * @return string Message state (visible, moderated, deleted)
	 */
	public function getProfilePostInsertMessageState(array $user, array $viewingUser = null)
	{
		$this->standardizeViewingUserReference($viewingUser);

		if ($viewingUser['user_id'] && XenForo_Permission::hasPermission($viewingUser['permissions'], 'profilePost', 'approveUnapprove'))
		{
			return 'visible';
		}
		else if (XenForo_Permission::hasPermission($viewingUser['permissions'], 'general', 'followModerationRules'))
		{
			return 'visible'; // TODO: follow profile-level settings when they exist
		}
		else
		{
			return 'moderated';
		}
	}

	/**
	 * Constant for fetching the info about the comment user.
	 *
	 * @var integer
	 */
	const FETCH_COMMENT_USER = 0x01;

	/**
	 * Constant for fetching the profile post for a comment
	 *
	 * @var integer
	 */
	const FETCH_COMMENT_PROFILE_POST = 0x02;

	/**
	 * Constant for fetching the deletion log info for the comment.
	 *
	 * @var integer
	 */
	const FETCH_COMMENT_DELETION_LOG = 0x04;

	/**
	 * Prepares a collection of profile post commment fetching related conditions into an SQL clause
	 *
	 * @param array $conditions List of conditions
	 * @param array $fetchOptions Modifiable set of fetch options (may have joins pushed on to it)
	 *
	 * @return string SQL clause (at least 1=1)
	 */
	public function prepareProfilePostCommentConditions(array $conditions, array &$fetchOptions)
	{
		$sqlConditions = array();
		$db = $this->_getDb();

		if (!empty($conditions['user_id']))
		{
			$sqlConditions[] = "profile_post_comment.user_id = " . $db->quote($conditions['user_id']);
		}

		if (!empty($conditions['profile_post_id']))
		{
			if (!is_array($conditions['profile_post_id']))
			{
				$conditions['profile_post_id'] = array($conditions['profile_post_id']);
			}

			$sqlConditions[] = "profile_post_comment.profile_post_id IN(" . $db->quote($conditions['profile_post_id']) . ")";
		}

		if (!empty($conditions['profile_post_comment_id']))
		{
			if (!is_array($conditions['profile_post_comment_id']))
			{
				$conditions['profile_post_comment_id'] = array($conditions['profile_post_comment_id']);
			}

			$sqlConditions[] = "profile_post_comment.profile_post_comment_id IN(" . $db->quote($conditions['profile_post_comment_id']) . ")";
		}

		if (!empty($conditions['comment_date']) && is_array($conditions['comment_date']))
		{
			list($operator, $cutOff) = $conditions['comment_date'];

			$this->assertValidCutOffOperator($operator);
			$sqlConditions[] = "profile_post_comment.comment_date $operator " . $db->quote($cutOff);
		}

		if (!empty($conditions['before_date']))
		{
			$sqlConditions[] = "profile_post_comment.comment_date < " . $db->quote($conditions['before_date']);
		}

		if (isset($conditions['deleted']) || isset($conditions['moderated']))
		{
			$sqlConditions[] = $this->prepareStateLimitFromConditions($conditions, 'profile_post_comment', 'message_state');
		}

		return $this->getConditionsForClause($sqlConditions);
	}

	/**
	 * Prepares the fetching options for profile post comments.
	 *
	 * @param array $fetchOptions
	 *
	 * @return array
	 */
	public function prepareProfilePostCommentFetchOptions(array $fetchOptions)
	{
		$selectFields = '';
		$joinTables = '';

		$db = $this->_getDb();

		if (!empty($fetchOptions['join']))
		{
			if ($fetchOptions['join'] & self::FETCH_COMMENT_USER)
			{
				$selectFields .= ',
					user.*,
					IF(user.username IS NULL, profile_post_comment.username, user.username) AS username';
				$joinTables .= '
					LEFT JOIN xf_user AS user ON
						(user.user_id = profile_post_comment.user_id)';
			}

			if ($fetchOptions['join'] & self::FETCH_COMMENT_PROFILE_POST)
			{
				$selectFields .= ',
					profile_post.*';
				$joinTables .= '
					INNER JOIN xf_profile_post AS profile_post ON
						(profile_post.profile_post_id = profile_post_comment.profile_post_id)';
			}

			if ($fetchOptions['join'] & self::FETCH_COMMENT_DELETION_LOG)
			{
				$selectFields .= ',
					deletion_log.delete_date, deletion_log.delete_reason,
					deletion_log.delete_user_id, deletion_log.delete_username';
				$joinTables .= '
					LEFT JOIN xf_deletion_log AS deletion_log ON
						(deletion_log.content_type = \'profile_post_comment\' AND deletion_log.content_id = profile_post_comment.profile_post_comment_id)';
			}

			if (isset($fetchOptions['likeUserId']))
			{
				if (empty($fetchOptions['likeUserId']))
				{
					$selectFields .= ',
						0 AS like_date';
				}
				else
				{
					$selectFields .= ',
						liked_content.like_date';
					$joinTables .= '
						LEFT JOIN xf_liked_content AS liked_content
							ON (liked_content.content_type = \'profile_post_comment\'
								AND liked_content.content_id = profile_post_comment.profile_post_comment_id
								AND liked_content.like_user_id = ' .$db->quote($fetchOptions['likeUserId']) . ')';
				}
			}
		}

		return array(
			'selectFields' => $selectFields,
			'joinTables'   => $joinTables
		);
	}

	/**
	 * Gets the specified profile post comment.
	 *
	 * @param integer $profilePostCommentId
	 * @param array $fetchOptions
	 *
	 * @return array|false
	 */
	public function getProfilePostCommentById($profilePostCommentId, array $fetchOptions = array())
	{
		$sqlClauses = $this->prepareProfilePostCommentFetchOptions($fetchOptions);

		return $this->_getDb()->fetchRow('
			SELECT profile_post_comment.*
			' . $sqlClauses['selectFields'] . '
			FROM xf_profile_post_comment AS profile_post_comment
			' . $sqlClauses['joinTables'] . '
			WHERE profile_post_comment.profile_post_comment_id = ?
		', $profilePostCommentId);
	}

	/**
	 * Gets profile post comments.
	 *
	 * @param array $ids
	 * @param array $fetchOptions
	 *
	 * @return array [id] => info
	 */
	public function getProfilePostComments(array $conditions = array(), array $fetchOptions = array())
	{
		$whereClause = $this->prepareProfilePostCommentConditions($conditions, $fetchOptions);
		$sqlClauses = $this->prepareProfilePostCommentFetchOptions($fetchOptions);

		return $this->fetchAllKeyed('
			SELECT profile_post_comment.*
			' . $sqlClauses['selectFields'] . '
			FROM xf_profile_post_comment AS profile_post_comment
			' . $sqlClauses['joinTables'] . '
			WHERE ' . $whereClause . '
		', 'profile_post_comment_id');
	}

	/**
	 * Gets the profile post comments with the specified IDs.
	 *
	 * @param array $ids
	 * @param array $fetchOptions
	 *
	 * @return array [id] => info
	 */
	public function getProfilePostCommentsByIds(array $ids, array $fetchOptions = array())
	{
		if (!$ids)
		{
			return array();
		}

		$sqlClauses = $this->prepareProfilePostCommentFetchOptions($fetchOptions);

		return $this->fetchAllKeyed('
			SELECT profile_post_comment.*
			' . $sqlClauses['selectFields'] . '
			FROM xf_profile_post_comment AS profile_post_comment
			' . $sqlClauses['joinTables'] . '
			WHERE profile_post_comment.profile_post_comment_id IN (' . $this->_getDb()->quote($ids) . ')
		', 'profile_post_comment_id');
	}

	/**
	 * Gets the profile post comments make by a particular user.
	 *
	 * @param array $ids
	 * @param array $fetchOptions
	 *
	 * @return array [id] => info
	 */
	public function getProfilePostCommentsByUserId($userId, array $fetchOptions = array())
	{
		$sqlClauses = $this->prepareProfilePostCommentFetchOptions($fetchOptions);

		return $this->fetchAllKeyed('
			SELECT profile_post_comment.*
			' . $sqlClauses['selectFields'] . '
			FROM xf_profile_post_comment AS profile_post_comment
			' . $sqlClauses['joinTables'] . '
			WHERE profile_post_comment.user_id = ?
		', 'profile_post_comment_id', $userId);
	}

	/**
	 * Gets all comments that belong to the specified post. If a limit is specified,
	 * more recent comments are returned.
	 *
	 * @param integer $profilePostId
	 * @param integer $beforeDate If specified, gets posts before specified date only
	 * @param array $fetchOptions
	 *
	 * @return array [id] => info
	 */
	public function getProfilePostCommentsByProfilePost($profilePostId, $beforeDate = 0, array $fetchOptions = array())
	{
		$conditions = $this->getPermissionBasedProfilePostConditions(array()) + array(
			'profile_post_id' => $profilePostId,
			'before_date' => $beforeDate
		);

		if ($conditions['deleted'])
		{
			if (isset($fetchOptions['join']))
			{
				$fetchOptions['join'] |= self::FETCH_COMMENT_DELETION_LOG;
			}
			else
			{
				$fetchOptions['join'] = self::FETCH_COMMENT_DELETION_LOG;
			}
		}

		$whereClause = $this->prepareProfilePostCommentConditions($conditions, $fetchOptions);
		$sqlClauses = $this->prepareProfilePostCommentFetchOptions($fetchOptions);
		$limitOptions = $this->prepareLimitFetchOptions($fetchOptions);

		$results = $this->fetchAllKeyed($this->limitQueryResults(
			'
				SELECT profile_post_comment.*
				' . $sqlClauses['selectFields'] . '
				FROM xf_profile_post_comment AS profile_post_comment
				' . $sqlClauses['joinTables'] . '
				WHERE ' . $whereClause . '
				ORDER BY profile_post_comment.comment_date DESC
			', $limitOptions['limit'], $limitOptions['offset']
		), 'profile_post_comment_id');

		return array_reverse($results, true);
	}

	/**
	 * Gets profile post comment IDs in the specified range. The IDs returned will be those immediately
	 * after the "start" value (not including the start), up to the specified limit.
	 *
	 * @param integer $start IDs greater than this will be returned
	 * @param integer $limit Number of posts to return
	 *
	 * @return array List of IDs
	 */
	public function getProfilePostCommentIdsInRange($start, $limit)
	{
		$db = $this->_getDb();

		return $db->fetchCol($db->limit('
			SELECT profile_post_comment_id
			FROM xf_profile_post_comment
			WHERE profile_post_comment_id > ?
			ORDER BY profile_post_comment_id
		', $limit), $start);
	}

	/**
	 * Merges comments into existing profile post data.
	 *
	 * @param array $profilePosts
	 * @param array $fetchOptions
	 *
	 * @return array Posts with comments merged
	 */
	public function addProfilePostCommentsToProfilePosts(array $profilePosts, array $fetchOptions = array(), array $viewingUser = null)
	{
		$this->standardizeViewingUserReference($viewingUser);
		$conditions = $this->getPermissionBasedProfilePostConditions(array(), $viewingUser);

		$commentIdMap = array();
		foreach ($profilePosts AS &$profilePost)
		{
			$cachedCount = 0;

			if (is_numeric(substr($profilePost['latest_comment_ids'], 0, 1))) // old format IDs comma separated
			{
				$ids = explode(',', $profilePost['latest_comment_ids']);
				foreach ($ids AS $commentId)
				{
					$commentIdMap[intval($commentId)] = $profilePost['profile_post_id'];
				}

				$cachedCount = count($ids);
			}
			else // new format [commentid] => [state, user_id]
			{
				if ($ids = @unserialize($profilePost['latest_comment_ids']))
				{
					$cachedCount = count($ids);

					$grouped = array();

					foreach ($ids AS $commentId => $state)
					{
						$commentId = intval($commentId);
						$profilePostId = $profilePost['profile_post_id'];

						switch ($state[0])
						{
							case 'visible':
								$grouped[$profilePostId][$commentId] = $commentId;
								break;

							case 'moderated':
								if ($conditions['moderated'])
								{
									if ((is_int($conditions['moderated']) && $conditions['moderated'] === $state[1])
										|| $conditions['moderated'] === true
									)
									{
										$grouped[$profilePostId][$commentId] = $commentId;
									}
								}
								break;

							case 'deleted':
								if ($conditions['deleted'])
								{
									$grouped[$profilePostId][$commentId] = $commentId;
								}
								break;
						}
					}

					foreach ($grouped AS $profilePostId => $group)
					{
						$group = array_slice($group, -3, 3, true); // display a maximum of 3 comments per profile post
						foreach ($group AS $commentId)
						{
							$commentIdMap[$commentId] = $profilePostId;
						}
					}
				}
			}

			$profilePost['cached_count'] = $cachedCount;
			$profilePost['do_recount'] = false;

			if ($profilePost['comment_count'] <= 3 && $profilePost['cached_count'] >= 3)
			{
				if ($conditions['moderated'] === true || $conditions['deleted'] === true)
				{
					$profilePost['do_recount'] = true;
				}
			}

			$profilePost['comments'] = array();
		}

		if ($commentIdMap)
		{
			$conditions = $conditions + array(
				'profile_post_comment_id' => array_keys($commentIdMap)
			);

			if ($conditions['deleted'])
			{
				if (isset($fetchOptions['join']))
				{
					$fetchOptions['join'] |= self::FETCH_COMMENT_DELETION_LOG;
				}
				else
				{
					$fetchOptions['join'] = self::FETCH_COMMENT_DELETION_LOG;
				}
			}

			$comments = $this->getProfilePostComments($conditions, $fetchOptions);
			foreach ($commentIdMap AS $commentId => $profilePostId)
			{
				if (isset($comments[$commentId]))
				{
					if (!isset($profilePosts[$profilePostId]['first_shown_comment_date']))
					{
						$profilePosts[$profilePostId]['first_shown_comment_date'] = $comments[$commentId]['comment_date'];
					}
					$profilePosts[$profilePostId]['comments'][$commentId] = $comments[$commentId];
				}
			}
		}

		return $profilePosts;
	}

	public function countProfilePostComments(array $profilePostIds, array $viewingUser = null)
	{
		if (!$profilePostIds)
		{
			return array();
		}

		$this->standardizeViewingUserReference($viewingUser);
		$conditions = $this->getPermissionBasedProfilePostConditions(array(), $viewingUser);

		$states = array('visible');
		if ($conditions['moderated'] === true)
		{
			$states[] = 'moderated';
		}
		if ($conditions['deleted'] === true)
		{
			$states[] = 'deleted';
		}

		$db = $this->_getDb();

		return $db->fetchPairs('
			SELECT profile_post_id, COUNT(*)
			FROM xf_profile_post_comment
			WHERE message_state IN(' . $db->quote($states) . ')
				AND profile_post_id IN(' . $db->quote($profilePostIds) . ')
			GROUP BY profile_post_id
		');
	}

	/**
	 * Gets the user IDs that have commented on a profile post.
	 *
	 * @param integer $profilePostId
	 *
	 * @return array
	 */
	public function getProfilePostCommentUserIds($profilePostId, $messageState = null)
	{
		$stateClause = '';
		if ($messageState !== null)
		{
			if (!is_array($messageState))
			{
				$messageState = array($messageState);
			}
			$stateClause = 'AND message_state IN(' . $this->_getDb()->quote($messageState) . ')';
		}

		return $this->_getDb()->fetchCol('
			SELECT DISTINCT user_id
			FROM xf_profile_post_comment
			WHERE profile_post_id = ?
				' . $stateClause . '
		', $profilePostId);
	}

	/**
	 * Determines if the given profile post can be viewed. This does not
	 * check user profile viewing permissions.
	 *
	 * @param array $comment
	 * @param array $profilePost
	 * @param array $user
	 * @param string $errorPhraseKey By ref. More specific error, if available.
	 * @param array|null $viewingUser Viewing user reference
	 *
	 * @return boolean
	 */
	public function canViewProfilePostComment(array $comment, array $profilePost, array $user, &$errorPhraseKey = '', array $viewingUser = null)
	{
		$this->standardizeViewingUserReference($viewingUser);

		if (!XenForo_Permission::hasPermission($viewingUser['permissions'], 'profilePost', 'view'))
		{
			return false;
		}

		if ($comment['message_state'] == 'moderated'
			&& !XenForo_Permission::hasPermission($viewingUser['permissions'], 'profilePost', 'viewModerated')
		)
		{
			if (!$viewingUser['user_id'] || $viewingUser['user_id'] != $comment['user_id'])
			{
				return false;
			}
		}
		else if ($comment['message_state'] == 'deleted'
			&& !XenForo_Permission::hasPermission($viewingUser['permissions'], 'profilePost', 'viewDeleted')
		)
		{
			return false;
		}

		return $this->canViewProfilePost($profilePost, $user, $errorPhraseKey, $viewingUser);
	}

	/**
	 * Determines if the given profile post comment can be edited. This does not
	 * check parent (viewing) permissions.
	 *
	 * @param array $comment
	 * @param array $profilePost
	 * @param array $user
	 * @param string $errorPhraseKey By ref. More specific error, if available.
	 * @param array|null $viewingUser Viewing user reference
	 *
	 * @return boolean
	 */
	public function canEditProfilePostComment(array $comment, array $profilePost, array $user, &$errorPhraseKey = '', array $viewingUser = null)
	{
		$this->standardizeViewingUserReference($viewingUser);

		if (!$viewingUser['user_id'])
		{
			return false;
		}

		if ($viewingUser['user_id'] == $comment['user_id'])
		{
			return XenForo_Permission::hasPermission($viewingUser['permissions'], 'profilePost', 'editOwn');
		}
		else
		{
			return XenForo_Permission::hasPermission($viewingUser['permissions'], 'profilePost', 'editAny');
		}
	}

	/**
	 * Determines if the given profile post comment can be deleted. This does not
	 * check parent (viewing) permissions.
	 *
	 * @param array $comment
	 * @param array $profilePost
	 * @param array $user
	 * @param string $errorPhraseKey By ref. More specific error, if available.
	 * @param array|null $viewingUser Viewing user reference
	 *
	 * @return boolean
	 */
	public function canDeleteProfilePostComment(array $comment, array $profilePost, array $user, $deleteType = 'soft', &$errorPhraseKey = '', array $viewingUser = null)
	{
		$this->standardizeViewingUserReference($viewingUser);

		if (!$viewingUser['user_id'])
		{
			return false;
		}

		if ($deleteType != 'soft' && !XenForo_Permission::hasPermission($viewingUser['permissions'], 'profilePost', 'hardDeleteAny'))
		{
			return false;
		}

		if (XenForo_Permission::hasPermission($viewingUser['permissions'], 'profilePost', 'deleteAny'))
		{
			return true;
		}

		return (
			(
				$viewingUser['user_id'] == $profilePost['profile_user_id']
				&& XenForo_Permission::hasPermission($viewingUser['permissions'], 'profilePost', 'manageOwn')
			)
			||
			(
				$viewingUser['user_id'] == $comment['user_id']
				&& XenForo_Permission::hasPermission($viewingUser['permissions'], 'profilePost', 'deleteOwn')
			)
		);
	}

	/**
	 * Determines if the given profile post comment can be undeleted. This does not
	 * check parent (viewing) permissions.
	 *
	 * @param array $comment
	 * @param array $profilePost
	 * @param array $user
	 * @param string $errorPhraseKey By ref. More specific error, if available.
	 * @param array|null $viewingUser Viewing user reference
	 *
	 * @return boolean
	 */
	public function canUndeleteProfilePostComment(array $comment, array $profilePost, array $user, &$errorPhraseKey = '', array $viewingUser = null)
	{
		$this->standardizeViewingUserReference($viewingUser);
		return ($viewingUser['user_id'] && XenForo_Permission::hasPermission($viewingUser['permissions'], 'profilePost', 'undelete'));
	}

	/**
	 * Determines if an action alert can be sent for this profile post comment. This does not
	 * check parent (viewing) permissions.
	 *
	 * @param array $comment
	 * @param array $profilePost
	 * @param array $user
	 * @param string $errorPhraseKey By ref. More specific error, if available.
	 * @param array|null $viewingUser Viewing user reference
	 *
	 * @return boolean
	 */
	public function canSendProfilePostCommentActionAlert(array $comment, array $profilePost, array $user, &$errorPhraseKey = '', array $viewingUser = null)
	{
		$this->standardizeViewingUserReference($viewingUser);

		if (!$viewingUser['user_id'] || $viewingUser['user_id'] == $comment['user_id'])
		{
			return false;
		}

		if ($comment['message_state'] != 'visible' || $profilePost['message_state'] != 'visible')
		{
			return false;
		}

		return (
			XenForo_Permission::hasPermission($viewingUser['permissions'], 'profilePost', 'deleteAny')
			|| XenForo_Permission::hasPermission($viewingUser['permissions'], 'profilePost', 'editAny')
		);
	}

	/**
	 * Determines if the given profile post comment can be approved/unapproved. This does not
	 * check parent (viewing) permissions.
	 *
	 * @param array $profilePost
	 * @param array $user
	 * @param string $errorPhraseKey By ref. More specific error, if available.
	 * @param array|null $viewingUser Viewing user reference
	 *
	 * @return boolean
	 */
	public function canApproveUnapproveProfilePostComment(array $comment, array $profilePost, array $user, &$errorPhraseKey = '', array $viewingUser = null)
	{
		$this->standardizeViewingUserReference($viewingUser);
		return ($viewingUser['user_id'] && XenForo_Permission::hasPermission($viewingUser['permissions'], 'profilePost', 'approveUnapprove'));
	}

	/**
	 * Determines if the given profile post comment can be warned. This does not
	 * check parent (viewing) permissions.
	 *
	 * @param array $comment
	 * @param array $profilePost
	 * @param array $user
	 * @param string $errorPhraseKey By ref. More specific error, if available.
	 * @param array|null $viewingUser Viewing user reference
	 *
	 * @return boolean
	 */
	public function canWarnProfilePostComment(array $comment, array $profilePost, array $user, &$errorPhraseKey = '', array $viewingUser = null)
	{
		if ($comment['warning_id'] || empty($comment['user_id']))
		{
			return false;
		}

		if (!empty($comment['is_admin']) || !empty($comment['is_moderator']))
		{
			return false;
		}

		$this->standardizeViewingUserReference($viewingUser);

		return ($viewingUser['user_id'] && XenForo_Permission::hasPermission($viewingUser['permissions'], 'profilePost', 'warn'));
	}

	/**
	 * Determines if the profile post comment can be liked with the given permissions.
	 * This does not check profile post viewing permissions.
	 *
	 * @param array $comment Profile post comment info
	 * @param array $profilePost Profile post info
	 * @param array $user User info for where profile post is posted
	 * @param string $errorPhraseKey Returned phrase key for a specific error
	 * @param array|null $viewingUser
	 *
	 * @return boolean
	 */
	public function canLikeProfilePostComment(array $comment, array $profilePost, array $user, &$errorPhraseKey = '', array $viewingUser = null)
	{
		$this->standardizeViewingUserReference($viewingUser);

		if (!$viewingUser['user_id'])
		{
			return false;
		}

		if ($comment['message_state'] != 'visible' || $profilePost['message_state'] != 'visible')
		{
			return false;
		}

		if ($comment['user_id'] == $viewingUser['user_id'])
		{
			$errorPhraseKey = 'liking_own_content_cheating';
			return false;
		}

		return XenForo_Permission::hasPermission($viewingUser['permissions'], 'profilePost', 'like');
	}

	/**
	 * Checks that the viewing user may report the specified profile post comment
	 *
	 * @param array $comment
	 * @param array $profilePost
	 * @param array $user
	 * @param string
	 * @param boolean $errorPhraseKey
	 * @param array|null $viewingUser
	 *
	 * @return boolean
	 */
	public function canReportProfilePostComment(array $comment, array $profilePost, array $user, &$errorPhraseKey = '', array $viewingUser = null)
	{
		return $this->getModelFromCache('XenForo_Model_User')->canReportContent($errorPhraseKey, $viewingUser);
	}



	public function getProfilePostCommentIdsForCache($profilePostId)
	{
		$data = $this->fetchAllKeyed($this->limitQueryResults('
			SELECT profile_post_comment_id, message_state, user_id
			FROM xf_profile_post_comment
			WHERE profile_post_id = ?
			ORDER BY comment_date DESC
		', 20), 'profile_post_comment_id', $profilePostId);

		$cache = array();
		$visibleCount = 0;

		foreach ($data AS $id => $row)
		{
			$cache[$id] = array($row['message_state'], $row['user_id']);
			if ($row['message_state'] == 'visible')
			{
				$visibleCount++;
			}

			if ($visibleCount === 3)
			{
				break;
			}
		}
		$cache = array_reverse($cache, true); // need last comments, but in oldest first order

		return $cache;
	}

	/**
	 * Gets the message state for a newly inserted profile post comment by the viewing user.
	 *
	 * @param array $user User whose profile post is being commented on
	 * @param array|null $viewingUser
	 *
	 * @return string Message state (visible, moderated, deleted)
	 */
	public function getProfilePostCommentInsertMessageState(array $user, array $viewingUser = null)
	{
		$this->standardizeViewingUserReference($viewingUser);

		if ($viewingUser['user_id'] && XenForo_Permission::hasPermission($viewingUser['permissions'], 'profilePost', 'approveUnapprove'))
		{
			return 'visible';
		}
		else if (XenForo_Permission::hasPermission($viewingUser['permissions'], 'general', 'followModerationRules'))
		{
			return 'visible'; // TODO: follow profile-level settings when they exist
		}
		else
		{
			return 'moderated';
		}
	}

	/**
	 * Prepares a profile post comment for display.
	 *
	 * @param array $comment
	 * @param array $profilePost
	 * @param array $user
	 * @param array|null $viewingUser
	 *
	 * @return array
	 */
	public function prepareProfilePostComment(array $comment, array $profilePost, array $user, array $viewingUser = null)
	{
		$this->standardizeViewingUserReference($viewingUser);

		$comment['canDelete'] = $this->canDeleteProfilePostComment($comment, $profilePost, $user, 'soft', $null, $viewingUser);
		$comment['canEdit'] = $this->canEditProfilePostComment($comment, $profilePost, $user, $null, $viewingUser);
		$comment['canLike'] = $this->canLikeProfilePostComment($comment, $profilePost, $user, $null, $viewingUser);
		$comment['canUndelete'] = ($comment['canDelete'] && $comment['message_state'] == 'deleted');
		$comment['canViewIps'] = $this->canViewIps($profilePost, $user, $null, $viewingUser);
		$comment['canWarn'] = $this->canWarnProfilePostComment($comment, $profilePost, $user, $null, $viewingUser);
		$comment['canReport'] = $this->canReportProfilePostComment($comment, $profilePost, $user, $null, $viewingUser);
		$comment['canApproveUnapprove'] = $this->canApproveUnapproveProfilePostComment($comment, $profilePost, $user, $null, $viewingUser);

		$comment['isDeleted'] = ($comment['message_state'] == 'deleted');
		$comment['isModerated'] = ($comment['message_state'] == 'moderated');

		$comment['canCleanSpam'] = (
			$comment['canDelete']
			&& $user['user_id']
			&& XenForo_Permission::hasPermission($viewingUser['permissions'], 'general', 'cleanSpam')
			&& $this->_getUserModel()->couldBeSpammer($comment)
		);

		if (!empty($comment['delete_date']))
		{
			$comment['deleteInfo'] = array(
				'user_id' => $comment['delete_user_id'],
				'username' => $comment['delete_username'],
				'date' => $comment['delete_date'],
				'reason' => $comment['delete_reason']
			);
		}

		if ($comment['likes'])
		{
			$comment['likeUsers'] = unserialize($comment['like_users']);
		}

		if (array_key_exists('user_group_id', $comment))
		{
			$comment = $this->getModelFromCache('XenForo_Model_User')->prepareUser($comment);
		}

		return $comment;
	}

	/**
	 * Fetch view parameters for profile posts, primarily for meta controls like warn, view IPs etc.
	 *
	 * @param array $profilePosts
	 * @param array $user (owner of the profile on which the profile posts are shown)
	 * @param array $viewingUser
	 *
	 * @return array
	 */
	public function getProfilePostViewParams(array $profilePosts, array $user, array $viewingUser = null)
	{
		$this->standardizeViewingUserReference($viewingUser);

		$userModel = $this->_getUserModel();

		return array(
			'profilePosts' => $profilePosts,
			'canViewIps'   => $userModel->canViewIps(),
			'canViewWarnings' => $userModel->canViewWarnings(),
			'canWarn' => $userModel->canWarnUser($user),
		);
	}

	/**
	 * Attempts to update any instances of an old username in like_users with a new username
	 *
	 * @param integer $oldUserId
	 * @param integer $newUserId
	 * @param string $oldUsername
	 * @param string $newUsername
	 */
	public function batchUpdateProfilePostLikeUser($oldUserId, $newUserId, $oldUsername, $newUsername)
	{
		$db = $this->_getDb();

		// note that xf_liked_content should have already been updated with $newUserId
		$db->query('
			UPDATE (
				SELECT content_id FROM xf_liked_content
				WHERE content_type = \'profile_post\'
				AND like_user_id = ?
			) AS temp
			INNER JOIN xf_profile_post AS profile_post ON (profile_post.profile_post_id = temp.content_id)
			SET like_users = REPLACE(like_users, ' .
			$db->quote('i:' . $oldUserId . ';s:8:"username";s:' . strlen($oldUsername) . ':"' . $oldUsername . '";') . ', ' .
			$db->quote('i:' . $newUserId . ';s:8:"username";s:' . strlen($newUsername) . ':"' . $newUsername . '";') . ')
		', $newUserId);
	}

	/**
	 * Attempts to update any instances of an old username in like_users with a new username
	 *
	 * @param integer $oldUserId
	 * @param integer $newUserId
	 * @param string $oldUsername
	 * @param string $newUsername
	 */
	public function batchUpdateProfilePostCommentLikeUser($oldUserId, $newUserId, $oldUsername, $newUsername)
	{
		$db = $this->_getDb();

		// note that xf_liked_content should have already been updated with $newUserId
		$db->query('
			UPDATE (
				SELECT content_id FROM xf_liked_content
				WHERE content_type = \'profile_post_comment\'
				AND like_user_id = ?
			) AS temp
			INNER JOIN xf_profile_post_comment AS profile_post_comment ON (profile_post_comment.profile_post_comment_id = temp.content_id)
			SET like_users = REPLACE(like_users, ' .
			$db->quote('i:' . $oldUserId . ';s:8:"username";s:' . strlen($oldUsername) . ':"' . $oldUsername . '";') . ', ' .
			$db->quote('i:' . $newUserId . ';s:8:"username";s:' . strlen($newUsername) . ':"' . $newUsername . '";') . ')
		', $newUserId);
	}

	public function sendModeratorActionAlertForProfilePost($action, array $profilePost, array $profileUser, $reason = '', array $extra = array(), $alertUserId = null)
	{
		$extra = array_merge(array(
			'profileUser' => $profileUser,
			'profileLink' => XenForo_Link::buildPublicLink('members', $profileUser),
			'link' => XenForo_Link::buildPublicLink('profile-posts', $profilePost),
			'reason' => $reason
		), $extra);

		if ($alertUserId === null)
		{
			$alertUserId = $profilePost['user_id'];
		}

		if (!$alertUserId)
		{
			return false;
		}

		XenForo_Model_Alert::alert(
			$alertUserId,
			0, '',
			'user', $alertUserId,
			'profile_post_' . $action,
			$extra
		);
		return true;
	}

	public function sendModeratorActionAlertForProfilePostComment($action, array $comment, array $profilePost, array $profileUser, $reason = '', array $extra = array(), $alertUserId = null)
	{
		$extra = array_merge(array(
			'profilePost' => $profilePost,
			'profileUser' => $profileUser,
			'profileLink' => XenForo_Link::buildPublicLink('members', $profileUser),
			'link' => XenForo_Link::buildPublicLink('profile-posts/comments', $comment),
			'profilePostLink' => XenForo_Link::buildPublicLink('profile-posts', $profilePost),
			'reason' => $reason
		), $extra);

		if ($alertUserId === null)
		{
			$alertUserId = $comment['user_id'];
		}

		if (!$alertUserId)
		{
			return false;
		}

		XenForo_Model_Alert::alert(
			$alertUserId,
			0, '',
			'user', $alertUserId,
			'profile_post_comment_' . $action,
			$extra
		);
		return true;
	}

	public function alertTaggedMembers(
		array $profilePost, array $profileUser, array $tagged, array $alreadyAlerted = array(),
		$commentId = 0, array $taggingUser = null
	)
	{
		$userIds = XenForo_Application::arrayColumn($tagged, 'user_id');
		$userIds = array_diff($userIds, $alreadyAlerted);
		$alertedUserIds = array();

		if (!$taggingUser)
		{
			$taggingUser = $profilePost;
		}

		if ($userIds)
		{
			$userModel = $this->_getUserModel();
			$users = $userModel->getUsersByIds($userIds, array(
				'join' => XenForo_Model_User::FETCH_USER_OPTION
					| XenForo_Model_User::FETCH_USER_PROFILE
					| XenForo_Model_User::FETCH_USER_PERMISSIONS
			));
			foreach ($users AS $user)
			{
				if (isset($alertedUserIds[$user['user_id']]))
				{
					continue;
				}

				$user['permissions'] = XenForo_Permission::unserializePermissions($user['global_permission_cache']);

				if ($user['user_id'] != $taggingUser['user_id']
					&& !$userModel->isUserIgnored($user, $profilePost['user_id'])
					&& !$userModel->isUserIgnored($user, $profileUser['user_id'])
					&& XenForo_Model_Alert::userReceivesAlert($user, 'profile_post', 'tag')
					&& $this->canViewProfilePostAndContainer($profilePost, $profileUser, $null, $user)
				)
				{
					$alertedUserIds[$user['user_id']] = true;

					XenForo_Model_Alert::alert($user['user_id'],
						$taggingUser['user_id'], $taggingUser['username'],
						'profile_post' . ($commentId ? '_comment' : ''),
						$commentId ? $commentId : $profilePost['profile_post_id'],
						'tag'
					);
				}
			}
		}

		return array_keys($alertedUserIds);
	}

	/**
	 * @return XenForo_Model_User
	 */
	protected function _getUserModel()
	{
		return $this->getModelFromCache('XenForo_Model_User');
	}

	/**
	 * @return XenForo_Model_UserProfile
	 */
	protected function _getUserProfileModel()
	{
		return $this->getModelFromCache('XenForo_Model_UserProfile');
	}
}