<?php
/**
 * MyBB 1.8
 * Copyright 2014 MyBB Group, All Rights Reserved
 *
 * Website: http://www.mybb.com
 * License: http://www.mybb.com/about/license
 *
 */

// Disallow direct access to this file for security reasons
if(!defined("IN_MYBB"))
{
	die("Direct initialization of this file is not allowed.<br /><br />Please make sure IN_MYBB is defined.");
}

/**
 * Login handling class, provides common structure to handle login events.
 *
 */
class WarningsHandler extends DataHandler
{
	/**
	 * The language file used in the data handler.
	 *
	 * @var string
	 */
	public $language_file = 'datahandler_warnings';

	/**
	 * The prefix for the language variables used in the data handler.
	 *
	 * @var string
	 */
	public $language_prefix = 'warnings';

	/**
	 * The stored data for the warning being written.
	 *
	 * @var array
	 */
	public $write_warning_data = array();

	/**
	 * The stored data for the warning being retrieved.
	 *
	 * @var array
	 */
	private $read_warning_data = array();

	/**
	 * Friendly redirect action after inserting a new warning.
	 *
	 * @var string
	 */
	public $friendly_action = '';

	/**
	* Validate a warning user assets.
	*
	* @return boolean True when valid, false when invalid.
	*/
	function validate_user()
	{
		global $mybb;

		$warning = &$this->data;

		$user = get_user($warning['uid']);

		if(!$user)
		{
			$this->set_error('error_invalid_user');
			return false;
		}

		if($user['uid'] == $mybb->user['uid'])
		{
			$this->set_error('error_cannot_warn_self');
			return false;
		}

		if($user['warningpoints'] >= $mybb->settings['maxwarningpoints'])
		{
			$this->set_error('error_user_reached_max_warning');
			return false;
		}

		return true;
	}

	/**
	* Validate a warning post.
	*
	* @return boolean True when valid, false when invalid.
	*/
	function validate_post()
	{
		$warning = &$this->data;

		$post = get_post($warning['pid']);

		if(!$post)
		{
			$this->set_error('error_invalid_post');
			return false;
		}

		return true;
	}

	/**
	* Validate a warning notes.
	*
	* @return boolean True when valid, false when invalid.
	*/
	function validate_notes()
	{
		$warning = &$this->data;

		if(!trim($warning['notes']))
		{
			$this->set_error('error_no_note');
			return false;
		}

		return true;
	}

	/**
	* Validate maximum warnings per day for current user.
	*
	* @return boolean True when valid, false when invalid.
	*/
	function validate_maximum()
	{
		global $mybb, $db, $lang;

		if($mybb->usergroup['maxwarningsday'] != 0)
		{
			$timecut = TIME_NOW-60*60*24;
			$query = $db->simple_select("warnings", "COUNT(wid) AS given_today", "issuedby='{$mybb->user['uid']}' AND dateline>'$timecut'");
			$given_today = $db->fetch_field($query, "given_today");
			if($given_today >= $mybb->usergroup['maxwarningsday'])
			{
				$this->set_error('reached_max_warnings_day', array(my_number_format($mybb->usergroup['maxwarningsday'])));
				return false;
			}
		}

		return true;
	}

	/**
	* Validate warnings type.
	*
	* @return boolean True when valid, false when invalid.
	*/
	function validate_type()
	{
		global $mybb, $db;

		$warning = &$this->data;

		// Issuing a custom warning
		if($warning['type'] == 'custom')
		{
			if($mybb->settings['allowcustomwarnings'] == 0)
			{
				$this->set_error('error_cant_custom_warn');
				return false;
			}

			if(!$warning['custom_reason'])
			{
				$this->set_error('error_no_custom_reason');
				return false;
			}

			$warning['title'] = $warning['custom_reason'];

			if(!$warning['custom_points'] || $warning['custom_points'] > $mybb->settings['maxwarningpoints'] || $warning['custom_points'] < 0)
			{
				$this->set_error('error_invalid_custom_points', array(my_number_format($mybb->settings['maxwarningpoints'])));
				return false;
			}

			$warning['points'] = round($warning['custom_points']);

			// Build expiry date				
			if($warning['expires_period'] == "hours")
			{
				$warning['expires'] = $warning['expires']*3600 + TIME_NOW;
			}
			else if($warning['expires_period'] == "days")
			{
				$warning['expires'] = $warning['expires']*86400 + TIME_NOW;
			}
			else if($warning['expires_period'] == "weeks")
			{
				$warning['expires'] = $warning['expires']*604800 + TIME_NOW;
			}
			else if($warning['expires_period'] == "months")
			{
				$warning['expires'] = $warning['expires']*2592000 + TIME_NOW;
			}
			else if($warning['expires_period'] == "never")
			{
				$warning['expires'] = 0;
			}
			else
			{
				// unkown expires_period
				$this->set_error('error_invalid_expires_period');
				return false;
			}
		}
		// Using a predefined warning type
		else
		{
			$query = $db->simple_select("warningtypes", "*", "tid='".(int)$warning['type']."'");
			$this->warning_type = $db->fetch_array($query);

			if(!$this->warning_type)
			{
				$this->set_error('error_invalid_type');
				return false;
			}

			$warning['points'] = $this->warning_type['points'];
			$warning['title'] = '';
			$warning['expires'] = 0;
			
			if($this->warning_type['expirationtime'])
			{
				$warning['expires'] = TIME_NOW+$this->warning_type['expirationtime'];
			}
		}

		return true;
	}

	/**
	 * Validate a warning.
	 *
	 * @return boolean True when valid, false when invalid.
	 */
	function validate_warning()
	{
		global $plugins;

		$warning = &$this->data;

		// Verify all warning assets.
		$this->validate_user();
		$this->validate_maximum();
		$this->validate_notes();

		if(array_key_exists('pid', $warning))
		{
			$this->validate_post();
		}
		if(array_key_exists('type', $warning))
		{
			$this->validate_type();
		}

		$plugins->run_hooks("datahandler_warnings_validate_warning", $this);

		// We are done validating, return.
		$this->set_validated(true);

		if(count($this->get_errors()) > 0)
		{
			return false;
		}

		return true;
	}

	/**
	* Gets a valid warning from the DB engine.
	*
	* @param int $wid
	* @return array|bool array when valid, boolean false when invalid.
	*/
	function get($wid)
	{
		global $db;

		$wid = (int)$wid;
		if($wid <= 0)
		{
			return false;
		}

		$query = $db->simple_select("warnings", "*", "wid='".$wid."'");
		$this->read_warning_data = $db->fetch_array($query);

		if(!$this->read_warning_data['wid'])
		{
			return false;
		}

		return $this->read_warning_data;
	}

	/**
	* Expire old warnings in the database.
	*
	* @return boolean True when finished.
	*/
	function expire_warnings()
	{
		global $db;

		$users = array();

		$query = $db->query("
			SELECT w.wid, w.uid, w.points, u.warningpoints
			FROM ".TABLE_PREFIX."warnings w
			LEFT JOIN ".TABLE_PREFIX."users u ON (u.uid=w.uid)
			WHERE expires<".TIME_NOW." AND expires!=0 AND expired!=1
		");
		while($warning = $db->fetch_array($query))
		{
			$updated_warning = array(
				"expired" => 1
			);
			$db->update_query("warnings", $updated_warning, "wid='{$warning['wid']}'");

			if(array_key_exists($warning['uid'], $users))
			{
				$users[$warning['uid']] -= $warning['points'];
			}
			else
			{
				$users[$warning['uid']] = $warning['warningpoints']-$warning['points'];
			}
		}

		foreach($users as $uid => $warningpoints)
		{
			if($warningpoints < 0)
			{
				$warningpoints = 0;
			}

			$updated_user = array(
				"warningpoints" => (int)$warningpoints
			);
			$db->update_query("users", $updated_user, "uid='".(int)$uid."'");
		}

		return true;
	}

	/**
	* Updates an user warning details.
	*
	* @return array Updated user details.
	*/
	function update_user($method='insert')
	{
		global $db, $mybb, $lang, $cache, $groupscache;

		if($mybb->settings['maxwarningpoints'] < 1)
		{
			$mybb->settings['maxwarningpoints'] = 10;
		}

		if(!is_array($groupscache))
		{
			$groupscache = $cache->read("usergroups");
		}

		$warning = &$this->data;

		$user = get_user($warning['uid']);

		if($method == 'insert')
		{
			// Build warning level & ensure it doesn't go over 100.
			$current_level = round($user['warningpoints']/$mybb->settings['maxwarningpoints']*100);
			$this->new_warning_level = round(($user['warningpoints']+$warning['points'])/$mybb->settings['maxwarningpoints']*100);
			if($this->new_warning_level > 100)
			{
				$this->new_warning_level = 100;
			}

			// Update user
			$this->updated_user = array(
				"warningpoints" => $user['warningpoints']+$warning['points']
			);

			// Fetch warning level
			$query = $db->simple_select("warninglevels", "*", "percentage<={$this->new_warning_level}", array("order_by" => "percentage", "order_dir" => "desc"));
			$new_level = $db->fetch_array($query);

			if(!empty($new_level) && $new_level['lid'])
			{
				$expiration = 0;
				$action = my_unserialize($new_level['action']);

				if($action['length'] > 0)
				{
					$expiration = TIME_NOW+$action['length'];
				}

				switch($action['type'])
				{
					// Ban the user for a specified time
					case 1:
						// Fetch any previous bans for this user
						$query = $db->simple_select("banned", "*", "uid='{$user['uid']}' AND gid='{$action['usergroup']}' AND lifted>".TIME_NOW);
						$existing_ban = $db->fetch_array($query);

						// Only perform if no previous ban or new ban expires later than existing ban
						if(($expiration > $existing_ban['lifted'] && $existing_ban['lifted'] != 0) || $expiration == 0 || !$existing_ban['uid'])
						{
							if(!$warning['title'])
							{
								$warning['title'] = $this->warning_type['title'];
							}

							// Never lift the ban?
							if($action['length'] <= 0)
							{
								$bantime = '---';
							}
							else
							{
								$bantimes = fetch_ban_times();
								foreach($bantimes as $date => $string)
								{
									if($date == '---')
									{
										continue;
									}

									$time = 0;
									list($day, $month, $year) = explode('-', $date);
									if($day > 0)
									{
										$time += 60*60*24*$day;
									}

									if($month > 0)
									{
										$time += 60*60*24*30*$month;
									}

									if($year > 0)
									{
										$time += 60*60*24*365*$year;
									}

									if($time == $action['length'])
									{
										$bantime = $date;
										break;
									}
								}
							}

							$new_ban = array(
								"uid" => $user['uid'],
								"gid" => $action['usergroup'],
								"oldgroup" => $user['usergroup'],
								"oldadditionalgroups" => $db->escape_string($user['additionalgroups']),
								"olddisplaygroup" => $user['displaygroup'],
								"admin" => $mybb->user['uid'],
								"dateline" => TIME_NOW,
								"bantime" => $db->escape_string($bantime),
								"lifted" => $expiration,
								"reason" => $db->escape_string($warning['title'])
							);
							// Delete old ban for this user, taking details
							if($existing_ban['uid'])
							{
								$db->delete_query("banned", "uid='{$user['uid']}' AND gid='{$action['usergroup']}'");
								// Override new ban details with old group info
								$new_ban['oldgroup'] = $existing_ban['oldgroup'];
								$new_ban['oldadditionalgroups'] = $db->escape_string($existing_ban['oldadditionalgroups']);
								$new_ban['olddisplaygroup'] = $existing_ban['olddisplaygroup'];
							}

							$period = $lang->expiration_never;
							$ban_length = fetch_friendly_expiration($action['length']);

							if($ban_length['time'])
							{
								$lang_str = "expiration_".$ban_length['period'];
								$period = $lang->sprintf($lang->result_period, $ban_length['time'], $lang->$lang_str);
							}

							$group_name = $groupscache[$action['usergroup']]['title'];
							$this->friendly_action = $lang->sprintf($lang->redirect_warned_banned, $group_name, $period);

							$db->insert_query("banned", $new_ban);
							$this->updated_user['usergroup'] = $action['usergroup'];
							$this->updated_user['additionalgroups'] = '';
							$this->updated_user['displaygroup'] = 0;
						}
						break;
					// Suspend posting privileges
					case 2:
						// Only perform if the expiration time is greater than the users current suspension period
						if($expiration == 0 || $expiration > $user['suspensiontime'])
						{
							if(($user['suspensiontime'] != 0 && $user['suspendposting']) || !$user['suspendposting'])
							{
								$period = $lang->expiration_never;
								$ban_length = fetch_friendly_expiration($action['length']);

								if($ban_length['time'])
								{
									$lang_str = "expiration_".$ban_length['period'];
									$period = $lang->sprintf($lang->result_period, $ban_length['time'], $lang->$lang_str);
								}

								$this->friendly_action = $lang->sprintf($lang->redirect_warned_suspended, $period);

								$this->updated_user['suspensiontime'] = $expiration;
								$this->updated_user['suspendposting'] = 1;
							}
						}
						break;
					// Moderate new posts
					case 3:
						// Only perform if the expiration time is greater than the users current suspension period
						if($expiration == 0 || $expiration > $user['moderationtime'])
						{
							if(($user['moderationtime'] != 0 && $user['moderateposts']) || !$user['suspendposting'])
							{
								$period = $lang->expiration_never;
								$ban_length = fetch_friendly_expiration($action['length']);

								if($ban_length['time'])
								{
									$lang_str = "expiration_".$ban_length['period'];
									$period = $lang->sprintf($lang->result_period, $ban_length['time'], $lang->$lang_str);
								}

								$this->friendly_action = $lang->sprintf($lang->redirect_warned_moderate, $period);

								$this->updated_user['moderationtime'] = $expiration;
								$this->updated_user['moderateposts'] = 1;
							}
						}
						break;
				}
			}
		}
		else
		{
			// Warning is still active, lower users point count
			if($warning['expired'] != 1)
			{
				$new_warning_points = $user['warningpoints']-$warning['points'];
				if($new_warning_points < 0)
				{
					$new_warning_points = 0;
				}

				$this->updated_user = array(
					"warningpoints" => $new_warning_points
				);


				// check if we need to revoke any consequences with this warning
				$current_level = round($user['warningpoints']/$mybb->settings['maxwarningpoints']*100);
				$this->new_warning_level = round($new_warning_points/$mybb->settings['maxwarningpoints']*100);
				$query = $db->simple_select("warninglevels", "action", "percentage>{$this->new_warning_level} AND percentage<=$current_level");
				if($db->num_rows($query))
				{
					// we have some warning levels we need to revoke
					$max_expiration_times = $check_levels = array();
					find_warnlevels_to_check($query, $max_expiration_times, $check_levels);

					// now check warning levels already applied to this user to see if we need to lower any expiration times
					$query = $db->simple_select("warninglevels", "action", "percentage<={$this->new_warning_level}");
					$lower_expiration_times = $lower_levels = array();
					find_warnlevels_to_check($query, $lower_expiration_times, $lower_levels);

					// now that we've got all the info, do necessary stuff
					for($i = 1; $i <= 3; ++$i)
					{
						if($check_levels[$i])
						{
							switch($i)
							{
								case 1: // Ban
									// we'll have to resort to letting the admin/mod remove the ban manually, since there's an issue if stacked bans are in force...
									continue 2;
								case 2: // Revoke posting
									$current_expiry_field = 'suspensiontime';
									$current_inforce_field = 'suspendposting';
									break;
								case 3:
									$current_expiry_field = 'moderationtime';
									$current_inforce_field = 'moderateposts';
									break;
							}

							// if the thing isn't in force, don't bother with trying to update anything
							if(!$user[$current_inforce_field])
							{
								continue;
							}

							if($lower_levels[$i])
							{
								// lessen the expiration time if necessary

								if(!$lower_expiration_times[$i])
								{
									// doesn't expire - enforce this
									$this->updated_user[$current_expiry_field] = 0;
									continue;
								}

								if($max_expiration_times[$i])
								{
									// if the old level did have an expiry time...
									if($max_expiration_times[$i] <= $lower_expiration_times[$i])
									{
										// if the lower expiration time is actually higher than the upper expiration time -> skip
										continue;
									}
									// both new and old max expiry times aren't infinite, so we can take a difference
									$expire_offset = ($lower_expiration_times[$i] - $max_expiration_times[$i]);
								}
								else
								{
									// the old level never expired, not much we can do but try to estimate a new expiry time... which will just happen to be starting from today...
									$expire_offset = TIME_NOW + $lower_expiration_times[$i];
									// if the user's expiry time is already less than what we're going to set it to, skip
									if($user[$current_expiry_field] <= $expire_offset)
									{
										continue;
									}
								}

								$this->updated_user[$current_expiry_field] = $user[$current_expiry_field] + $expire_offset;
								// double-check if it's expired already
								if($this->updated_user[$current_expiry_field] < TIME_NOW)
								{
									$this->updated_user[$current_expiry_field] = 0;
									$this->updated_user[$current_inforce_field] = 0;
								}
							}
							else
							{
								// there's no lower level for this type - remove the consequence entirely
								$this->updated_user[$current_expiry_field] = 0;
								$this->updated_user[$current_inforce_field] = 0;
							}
						}
					}
				}
			}
		}

		// Save updated details
		$db->update_query("users", $this->updated_user, "uid='{$user['uid']}'");

		$mybb->cache->update_moderators();

		return $this->updated_user;
	}

	/**
	* Inserts a warning into the database
	*
	* @return array Warning database details.
	*/
	function insert_warning()
	{
		global $db, $mybb, $plugins;

		$warning = &$this->data;

		$this->write_warning_data = array(
			"uid" => (int)$warning['uid'],
			"tid" => (int)$warning['type'],
			"pid" => isset($warning['pid']) ? (int)$warning['pid'] : 0,
			"title" => $db->escape_string($warning['title']),
			"points" => (int)$warning['points'],
			"dateline" => TIME_NOW,
			"issuedby" => $mybb->user['uid'],
			"expires" => (int)$warning['expires'],
			"expired" => 0,
			"revokereason" => '',
			"notes" => $db->escape_string($warning['notes'])
		);

		$this->write_warning_data['wid'] = $db->insert_query("warnings", $this->write_warning_data);

		$this->update_user();

		$plugins->run_hooks("datahandler_warnings_insert_warning", $this);

		return $this->write_warning_data;
	}

	/**
	* Updates a warning in the database
	*
	* @return array|false Warning database details.
	*/
	function update_warning()
	{
		global $db, $mybb, $plugins;

		$warning = &$this->data;

		$warning['wid'] = (int)$warning['wid'];
		if($warning['wid'] <= 0)
		{
			return false;
		}

		$this->write_warning_data = array(
			"expired" => 1,
			"daterevoked" => TIME_NOW,
			"revokedby" => $mybb->user['uid'],
			"revokereason" => $db->escape_string($warning['reason'])
		);

		$plugins->run_hooks("datahandler_warnings_update_warning", $this);

		$db->update_query("warnings", $this->write_warning_data, "wid='{$warning['wid']}'");

		$this->update_user('update');

		return $this->write_warning_data;
	}

}

