<?php

defined('HOSTCMS') || exit('HostCMS: access denied.');

/**
 * Completely Automated Public Turing test to tell Computers and Humans Apart.
 *
 * @package HostCMS 6\Core
 * @version 6.x
 * @author Kruglov Sergei
 * @author Hostmake LLC
 * @copyright © 2006, 2007, 2008, 2011 Kruglov Sergei, http://www.captcha.ru
 * @copyright © 2005-2013 ООО "Хостмэйк" (Hostmake LLC), http://www.hostcms.ru
 */
class Core_Captcha
{
	/**
	 * Путь к шрифтам
	 */
	protected $_fontsDir = 'hostcmsfiles/captcha/fonts';

	/**
	 * Порядок символов в шрифтах
	 */
	protected $_alphabet = '0123456789abcdefghijklmnopqrstuvwxyz';

	/**
	 * амплитуда искажения
	 */
	protected $_amplitudeDistortion = 7;

	/**
	 * Флаг, указывающий, слитно писать символы CAPTCHA или нет
	 */
	protected $_spaces = TRUE;

	/**
	 * Расширение рисунка CAPTCHA
	 */
	protected $_type = 'PNG';

	/**
	 * Минимальное значение радиуса многоугольника на фоне CAPTCHA
	 */
	protected $_polygonMinRadius = 10;

	/**
	 * Максимальное значение радиуса многоугольника на фоне CAPTCHA
	 */
	protected $_polygonMaxRadius = 15;

	/**
	 * Минимальное значение количества углов многоугольника на фоне CAPTCHA
	 */
	protected $_polygonMinCorners = 3;

	/**
	 * Максимальное значение количества углов многоугольника на фоне CAPTCHA
	 */
	protected $_polygonMaxCorners = 6;

	/**
	 * CAPTCHA config
	 * @var array
	 */
	static protected $_config = NULL;

	/**
	 * Constructor.
	 */
	public function __construct()
	{
		if (is_null(self::$_config))
		{
			self::$_config = Core::$config->get('core_captcha') + array(
				'allowedCharacters' => '23456789abcdeghkmnpqsuvxyz',
				//'color' => array(mt_rand(0, 100), mt_rand(0, 100), mt_rand(0, 100)),
				'backgroundColor' => array(mt_rand(230, 255), mt_rand(230, 255), mt_rand(230, 255)),
				'noise' => 10,
				'width' => 88,
				'height' => 31,
				// Минимальная длина строки
				'minLenght' => 4,
				// Максимальная длина строки
				'maxLenght' => 4,
				'fillBackground' => TRUE,
			);
		}
	}

	/**
	 * Set config
	 * @param string $name name
	 * @param string $value value
	 * @return self
	 */
	public function setConfig($name, $value)
	{
		self::$_config[$name] = $value;
		return $this;
	}

	/**
	 * Получить уникальный индекс для CAPTCHA
	 * @return int
	 */
	static public function getCaptchaId()
	{
		$max = 99999;

		$maxValue = isset($_SESSION)
			? count($_SESSION) + $max
			: $max;

		$captchaId = mt_rand(0, $maxValue);
		while (isset($_SESSION['captcha_' + $captchaId]))
		{
			$captchaId = mt_rand(0, $maxValue);
		}

		return $captchaId;
	}

	/**
	 * Check if CAPTCHA is valid
	 * @param int $captchaId ID of CAPTCHA
	 * @param string $value value
	 * @return boolean
	 */
	static public function valid($captchaId, $value)
	{
		$captchaId = strval($captchaId);
		$value = strval($value);

		Core_Session::start();

		if (isset($_SESSION['captcha_' . $captchaId]))
		{
			$return = $value == $_SESSION['captcha_' . $captchaId];

			unset ($_SESSION['captcha_' . $captchaId]);
			return $return;
		}

		return FALSE;
	}

	/**
	 * Create value for CAPTCHA
	 * @return string 
	 */
	static public function createValue()
	{
		$length = mt_rand(self::$_config['minLenght'], self::$_config['maxLenght']);

		$allowedCharactersLength = strlen(self::$_config['allowedCharacters']);

		while (TRUE)
		{
			$value = '';
			for($i = 0; $i < $length; $i++)
			{
				$value .= self::$_config['allowedCharacters'][mt_rand(0, $allowedCharactersLength - 1)];
			}

			// Исключаем сочетания символов, сложные для распознавания пользователем
			if (!preg_match('/cp|cb|ck|c6|c9|rn|rm|mm|co|do|cl|db|qp|qb|dp|ww/u', $value))
			{
				break;
			}
		}

		return $value;
	}

	/**
	 * Построения изображения CAPTCHA и помещения его текста в сессию
	 *
	 * @param int $captchaId - уникальный номер CAPTCHA
	 */
	public function build($captchaId)
	{
		$ind = 0;

		$width = 120;
		$height = 50;

		// Цвет текста
		$foreground_color = isset(self::$_config['color'])
			? self::$_config['color']
			: array(mt_rand(0, 100), mt_rand(0, 100), mt_rand(0, 100));

		// Цвет фона
		$backgroundColor = self::$_config['backgroundColor'];

		// ШРИФТЫ
		$fonts = array();
		$_fontsDir = CMS_FOLDER . $this->_fontsDir;

		if (is_dir($_fontsDir) && !is_link($_fontsDir))
		{
			// Открываем директорию со шрифтами
			if ($handle = opendir($_fontsDir))
			{
				// Составляем массив шрифтов
				while (FALSE !== ($file = readdir($handle)))
				{
					if (preg_match('/\.png$/iu', $file))
					{
						$fonts[] = $_fontsDir . '/' . $file;
					}
				}

				closedir($handle);
			}
		}

		// Случайный шрифт
		$font = imagecreatefrompng($fonts[mt_rand(0, count($fonts) - 1)]);

		// Устанавливает режим смешивания для изображения
		imageAlphaBlending($font, FALSE);

		// Применяется для установки прозрачности изображений в формате PNG
		imageSaveAlpha($font, TRUE);

		$fontfile_width = imagesx($font);
		$fontfile_height = imagesy($font) - 1;

		// Формируем массив с указанием, с какого места начинается написание символа и в какой позиции оно заканчивается
		$font_metrics = array();
		$symbol = 0;

		// Флаг, указывающий, прочитан ли символ
		$characterRead = FALSE;

		// Загрузка шрифта
		$alphabetLength = strlen($this->_alphabet);
		for ($i = 0; $i < $fontfile_width && $symbol < $alphabetLength; $i++)
		{
			$transparent = (imagecolorat($font, $i, 0) >> 24) == 127;

			$smbl = $this->_alphabet[$symbol];

			// Символ еще не прочитан и позиция курсора не на пустом пространстве
			if (!$characterRead && !$transparent)
			{
				// Начало символа
				$font_metrics[$smbl]['start'] = $i;

				$characterRead = TRUE;
				continue;
			}

			// Символ прочитан и курсор оказался на пустом пространстве
			if ($characterRead && $transparent)
			{
				// Конец символа
				$font_metrics[$smbl]['end'] = $i;
				$characterRead = FALSE;

				// Следующий символ
				$symbol++;
				continue;
			}
		}

		$img = imagecreatetruecolor($width, $height);

		imagealphablending($img, TRUE);

		$white = imagecolorallocate($img, 255, 255, 255);
		imagefilledrectangle($img, 0, 0, $width - 1, $height - 1, $white);

		Core_Session::start();

		// Записываем в сессию строку
		$value = $_SESSION['captcha_' . $captchaId] = $this->createValue();

		Core_Session::close();

		$length = strlen($value);

		// Рисуем текст CAPTCHA
		$x = 1;

		for ($i = 0; $i < $length; $i++)
		{
			$m = $font_metrics[$value[$i]];

			$y = mt_rand(- $this->_amplitudeDistortion, $this->_amplitudeDistortion) + ($height - $fontfile_height) / 2 + 2;

			if (!$this->_spaces)
			{
				$shift = 0;

				if ($i > 0)
				{
					$shift = 10000;

					for ($sy = 7; $sy < $fontfile_height -20; $sy += 1)
					{
						for ($sx = $m['start'] - 1; $sx < $m['end']; $sx += 1)
						{
							$rgb = imagecolorat($font, $sx, $sy);
							$opacity = $rgb >> 24;
							$opacity = $rgb;

							if ($opacity < 127)
							{
								$left = $sx - $m['start'] + $x;
								$py = $sy + $y;

								if ($py > $height)
								{
									break;
								}

								for ($px = min($left, $width - 1); $px > $left -12 && $px >= 0; $px -= 1)
								{
									$color = imagecolorat($img, $px, $py) & 0xff;

									if ($color + $opacity < 170)
									{
										if ($shift > $left - $px)
										{
											$shift = $left - $px;
										}

										break;
									}
								}

								break;
							}
						}
					}

					if ($shift == 10000)
					{
						$shift = mt_rand(4, 6);
					}
				}
			}
			else
			{
				$shift = 1;
			}

			// Наносим  символ на рисунок CAPTCHA
			imagecopy($img, $font, $x, mt_rand(4, 10), $m['start'], 1, $m['end'] - $m['start'], $fontfile_height);

			// Курсор после последнего нанесенного символа
			$x += $m['end'] - $m['start'] - $shift;
		}
		// Пока за пределами изображения
		while ($x >= $width - 10);

		$center = $x / 2;

		// Итоговое изображение CAPTCHA
		$img2 = imagecreatetruecolor($width, $height);

		imagefilledrectangle($img2, 0, 0, $width - 1, $height - 1, imagecolorallocate($img2, $backgroundColor[0], $backgroundColor[1], $backgroundColor[2]));

		// Использовать генерацию фонов
		if (self::$_config['fillBackground'])
		{
			// Радиус
			$radius = mt_rand($this->_polygonMinRadius, $this->_polygonMaxRadius);

			// Количество углов многоугольника
			$corners = mt_rand($this->_polygonMinCorners, $this->_polygonMaxCorners);

			$t = 0;

			// Двигаемся сверху вниз по изображению
			for ($y = 0; $y < $height + 3 * $radius; $y += sqrt(3) * $radius / 2)
			{
				$t++;

				// Смещение многоугольника вправо на каждой второй строке на полтора радиуса
				$x = fmod($t, 2) == 0 ? 0 : 1.5 * $radius;

				// Двигаемся слева направа по изображению
				for ($x; $x < $width + 2 * $radius; $x += 3 * $radius)
				{
					// Наносим многоугольники
					$this->_dawPolygon($img2, $x, $y, $radius, $corners, $ind);
				}
			}
		}

		// частоты
		$rand1 = mt_rand(700000, 1000000) / 14000000;
		$rand2 = mt_rand(700000, 1000000) / 14000000;
		$rand3 = mt_rand(700000, 1000000) / 14000000;
		$rand4 = mt_rand(700000, 1000000) / 14000000;

		// фазы
		$rand5 = mt_rand(0, 3141592) / 1000000;
		$rand6 = mt_rand(0, 3141592) / 1000000;
		$rand7 = mt_rand(0, 3141592) / 1000000;
		$rand8 = mt_rand(0, 3141592) / 1000000;

		// амплитуды
		$rand9 = mt_rand(100, 250) / 100;
		$rand10 = mt_rand(100, 250) / 100;

		// Искажение
		for ($x = 0; $x < $width; $x++)
		{
			for ($y = 0; $y < $height; $y++)
			{
				$sx = $x + (sin($x * $rand1 + $rand5) + sin($y * $rand3 + $rand6)) * $rand9 - $width / 2 + $center +1;
				$sy = $y + (sin($x * $rand2 + $rand7) + sin($y * $rand4 + $rand8)) * $rand10;

				if ($sx < 0 || $sy < 0 || $sx >= $width - 1 || $sy >= $height - 1)
				{
					continue;
				}
				else
				{
					$color = (imagecolorat($img, $sx, $sy) >> 16) & 0xFF;
					$color_x = (imagecolorat($img, $sx + 1, $sy) >> 16) & 0xFF;
					$color_y = (imagecolorat($img, $sx, $sy + 1) >> 16) & 0xFF;
					$color_xy = (imagecolorat($img, $sx + 1, $sy + 1) >> 16) & 0xFF;
				}

				if ($color == 255 && $color_x == 255 && $color_y == 255 && $color_xy == 255)
				{
					continue;
				}
				else
				if ($color == 0 && $color_x == 0 && $color_y == 0 && $color_xy == 0)
				{
					$newred = $foreground_color[0];
					$newgreen = $foreground_color[1];
					$newblue = $foreground_color[2];
				}
				else
				{
					$frsx = $sx -floor($sx);
					$frsy = $sy -floor($sy);
					$frsx1 = 1 - $frsx;
					$frsy1 = 1 - $frsy;

					$newcolor = ($color * $frsx1 * $frsy1 + $color_x * $frsx * $frsy1 + $color_y * $frsx1 * $frsy + $color_xy * $frsx * $frsy);

					if ($newcolor > 255)
					$newcolor = 255;
					$newcolor = $newcolor / 255;
					$newcolor0 = 1 - $newcolor;

					$newred = $newcolor0 * $foreground_color[0] + $newcolor * $backgroundColor[0];
					$newgreen = $newcolor0 * $foreground_color[1] + $newcolor * $backgroundColor[1];
					$newblue = $newcolor0 * $foreground_color[2] + $newcolor * $backgroundColor[2];
				}

				imagesetpixel($img2, $x, $y,
					imagecolorallocate($img2, $newred, $newgreen, $newblue)
				);
			}
		}

		// Шум
		// Случайного цвета
		for($i = 0; $i < $height * self::$_config['noise']; $i++)
		{
			imagesetpixel($img2, mt_rand(0, $width), mt_rand(0, $height),
				imagecolorallocate($img2, mt_rand(0, 100), mt_rand(0, 100), mt_rand(0, 100))
			);
		}

		// Цвета текста
		for($i = 0; $i < $height * self::$_config['noise']; $i++)
		{
			imagesetpixel($img2, mt_rand(0, $width), mt_rand(0, $height),
				imagecolorallocate($img2, $foreground_color[0], $foreground_color[1], $foreground_color[2])
			);
		}

		header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
		header('Cache-Control: no-store, no-cache, must-revalidate');
		header('Cache-Control: post-check=0, pre-check=0', FALSE);
		header('Pragma: no-cache');

		// Масштабируем базовое изображение до необходимого
		$img3 = imagecreatetruecolor(self::$_config['width'], self::$_config['height']);
		imagecopyresampled($img3, $img2, 0, 0, 0, 0, self::$_config['width'], self::$_config['height'], $width, $height);

		imagedestroy($img2);

		ob_start(array($this, 'contentLength'));

		$type = strtoupper($this->_type);
		if ($type == 'GIF')
		{
			header('Content-Type: image/gif');
			imagegif($img3);
		}
		elseif ($type == 'PNG')
		{
			header('Content-Type: image/png');
			imagepng($img3, NULL, defined('PNG_QUALITY') ? PNG_QUALITY : 9);
		}
		else
		{
			header('Content-Type: image/jpeg');
			imagejpeg($img3, NULL, defined('JPG_QUALITY') ? JPG_QUALITY : 100);
		}

		imagedestroy($img3);
		ob_end_flush();
	}

	/**
	 * Set Content-Length header value
	 * @param string $content content
	 * @return string
	 */
	public static function contentLength($content)
	{
		header('Content-Length: ' . strlen($content));
		return $content;
	}

	/**
	 * Рисование многоугольника на фоне для CAPTCHA
	 * @param resource $image
	 * @param int $center_x координата х центра многоугольника
	 * @param int $center_y координата у центра многоугольника
	 * @param int $radius радиус
	 * @param int $corners количество углов многоугольника
	 * @param int $ind номер массива цветов (0 - светлый / 1 - темный)
	 *
	 * @return bool
	 * @access private
	 */
	public function _dawPolygon($image, $center_x, $center_y, $radius, $corners, $ind)
	{
		// Количество углов
		$corners = $corners < 3 ? 3 : intval($corners);
		$corners = $corners > 10 ? 10 : intval($corners);

		$ind = intval($ind) > 1 ? 1 : 0;

		$radius = intval($radius);
		$center_x = intval($center_x);
		$center_y = intval($center_y);

		// Массив координат углов многоугольника
		$point = array();

		for ($f = 1; $f <= $corners; $f++)
		{
			// Координаты следующего угла многоугольника
			$point[] = $center_x + $radius * cos(2 * pi() * $f / $corners);
			$point[] = $center_y + $radius * sin(2 * pi() * $f / $corners);
		}

		// Случайный цвет
		$colors = array(array(mt_rand(200, 255), mt_rand(200, 255), mt_rand(200, 255)), array(mt_rand(0, 100), mt_rand(0, 100), mt_rand(0, 100)));

		$backgroundColor = $colors[$ind];
		$color = $colors[1 - $ind];

		$background = imagecolorallocate($image, $backgroundColor[0], $backgroundColor[1], $backgroundColor[2]);
		$foreground = imagecolorallocate($image, $color[0], $color[1], $color[2]);
		imagefilledpolygon($image, $point, $corners, $background);
		imagepolygon($image, $point, $corners, $foreground);

		return TRUE;
	}
}