<?php
/**
 * Akeeba Engine
 * The modular PHP5 site backup engine
 *
 * @copyright Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU GPL version 3 or, at your option, any later version
 * @package   akeebaengine
 *
 */

namespace Akeeba\Engine\Util;

// Protection against direct access
use Akeeba\Engine\Platform;

defined('AKEEBAENGINE') or die();

/**
 * PHP port of http://github.com/danpalmer/jquery.complexify.js
 * Retrieved from https://github.com/mcrumley/php-complexify/blob/master/src/Complexify/Complexify.php
 * Error reporting is based on https://github.com/kislyuk/node-complexify
 */
class Complexify
{
	private static $MIN_COMPLEXITY = 66;

	private static $MAX_COMPLEXITY = 120; //  25 chars, all charsets

	private static $CHARSETS = array(
		// Commonly Used
		////////////////////
			array(0x0020, 0x0020), // Space
			array(0x0030, 0x0039), // Numbers
			array(0x0041, 0x005A), // Uppercase
			array(0x0061, 0x007A), // Lowercase
			array(0x0021, 0x002F), // Punctuation
			array(0x003A, 0x0040), // Punctuation
			array(0x005B, 0x0060), // Punctuation
			array(0x007B, 0x007E), // Punctuation
		// Everything Else
		////////////////////
			array(0x0080, 0x00FF), // Latin-1 Supplement
			array(0x0100, 0x017F), // Latin Extended-A
			array(0x0180, 0x024F), // Latin Extended-B
			array(0x0250, 0x02AF), // IPA Extensions
			array(0x02B0, 0x02FF), // Spacing Modifier Letters
			array(0x0300, 0x036F), // Combining Diacritical Marks
			array(0x0370, 0x03FF), // Greek
			array(0x0400, 0x04FF), // Cyrillic
			array(0x0530, 0x058F), // Armenian
			array(0x0590, 0x05FF), // Hebrew
			array(0x0600, 0x06FF), // Arabic
			array(0x0700, 0x074F), // Syriac
			array(0x0780, 0x07BF), // Thaana
			array(0x0900, 0x097F), // Devanagari
			array(0x0980, 0x09FF), // Bengali
			array(0x0A00, 0x0A7F), // Gurmukhi
			array(0x0A80, 0x0AFF), // Gujarati
			array(0x0B00, 0x0B7F), // Oriya
			array(0x0B80, 0x0BFF), // Tamil
			array(0x0C00, 0x0C7F), // Telugu
			array(0x0C80, 0x0CFF), // Kannada
			array(0x0D00, 0x0D7F), // Malayalam
			array(0x0D80, 0x0DFF), // Sinhala
			array(0x0E00, 0x0E7F), // Thai
			array(0x0E80, 0x0EFF), // Lao
			array(0x0F00, 0x0FFF), // Tibetan
			array(0x1000, 0x109F), // Myanmar
			array(0x10A0, 0x10FF), // Georgian
			array(0x1100, 0x11FF), // Hangul Jamo
			array(0x1200, 0x137F), // Ethiopic
			array(0x13A0, 0x13FF), // Cherokee
			array(0x1400, 0x167F), // Unified Canadian Aboriginal Syllabics
			array(0x1680, 0x169F), // Ogham
			array(0x16A0, 0x16FF), // Runic
			array(0x1780, 0x17FF), // Khmer
			array(0x1800, 0x18AF), // Mongolian
			array(0x1E00, 0x1EFF), // Latin Extended Additional
			array(0x1F00, 0x1FFF), // Greek Extended
			array(0x2000, 0x206F), // General Punctuation
			array(0x2070, 0x209F), // Superscripts and Subscripts
			array(0x20A0, 0x20CF), // Currency Symbols
			array(0x20D0, 0x20FF), // Combining Marks for Symbols
			array(0x2100, 0x214F), // Letterlike Symbols
			array(0x2150, 0x218F), // Number Forms
			array(0x2190, 0x21FF), // Arrows
			array(0x2200, 0x22FF), // Mathematical Operators
			array(0x2300, 0x23FF), // Miscellaneous Technical
			array(0x2400, 0x243F), // Control Pictures
			array(0x2440, 0x245F), // Optical Character Recognition
			array(0x2460, 0x24FF), // Enclosed Alphanumerics
			array(0x2500, 0x257F), // Box Drawing
			array(0x2580, 0x259F), // Block Elements
			array(0x25A0, 0x25FF), // Geometric Shapes
			array(0x2600, 0x26FF), // Miscellaneous Symbols
			array(0x2700, 0x27BF), // Dingbats
			array(0x2800, 0x28FF), // Braille Patterns
			array(0x2E80, 0x2EFF), // CJK Radicals Supplement
			array(0x2F00, 0x2FDF), // Kangxi Radicals
			array(0x2FF0, 0x2FFF), // Ideographic Description Characters
			array(0x3000, 0x303F), // CJK Symbols and Punctuation
			array(0x3040, 0x309F), // Hiragana
			array(0x30A0, 0x30FF), // Katakana
			array(0x3100, 0x312F), // Bopomofo
			array(0x3130, 0x318F), // Hangul Compatibility Jamo
			array(0x3190, 0x319F), // Kanbun
			array(0x31A0, 0x31BF), // Bopomofo Extended
			array(0x3200, 0x32FF), // Enclosed CJK Letters and Months
			array(0x3300, 0x33FF), // CJK Compatibility
			array(0x3400, 0x4DB5), // CJK Unified Ideographs Extension A
			array(0x4E00, 0x9FFF), // CJK Unified Ideographs
			array(0xA000, 0xA48F), // Yi Syllables
			array(0xA490, 0xA4CF), // Yi Radicals
			array(0xAC00, 0xD7A3), // Hangul Syllables
			array(0xD800, 0xDB7F), // High Surrogates
			array(0xDB80, 0xDBFF), // High Private Use Surrogates
			array(0xDC00, 0xDFFF), // Low Surrogates
			array(0xE000, 0xF8FF), // Private Use
			array(0xF900, 0xFAFF), // CJK Compatibility Ideographs
			array(0xFB00, 0xFB4F), // Alphabetic Presentation Forms
			array(0xFB50, 0xFDFF), // Arabic Presentation Forms-A
			array(0xFE20, 0xFE2F), // Combining Half Marks
			array(0xFE30, 0xFE4F), // CJK Compatibility Forms
			array(0xFE50, 0xFE6F), // Small Form Variants
			array(0xFE70, 0xFEFE), // Arabic Presentation Forms-B
			array(0xFEFF, 0xFEFF), // Specials
			array(0xFF00, 0xFFEF), // Halfwidth and Fullwidth Forms
			array(0xFFF0, 0xFFFD)  // Specials
	);

	// Generated from 500 worst passwords and 370 Banned Twitter lists found at
	// @source http://www.skullsecurity.org/wiki/index.php/Passwords
	private static $BANLIST = array('0', '1111', '1212', '1234', '1313', '2000', '2112', '2222',
			'3333', '4128', '4321', '4444', '5150', '5555', '6666', '6969', '7777', 'aaaa',
			'alex', 'asdf', 'baby', 'bear', 'beer', 'bill', 'blue', 'cock', 'cool', 'cunt',
			'dave', 'dick', 'eric', 'fire', 'fish', 'ford', 'fred', 'fuck', 'girl', 'golf',
			'jack', 'jake', 'john', 'king', 'love', 'mark', 'matt', 'mike', 'mine', 'pass',
			'paul', 'porn', 'rock', 'sexy', 'shit', 'slut', 'star', 'test', 'time', 'tits',
			'wolf', 'xxxx', '11111', '12345', 'angel', 'apple', 'beach', 'billy', 'bitch',
			'black', 'boobs', 'booty', 'brian', 'bubba', 'buddy', 'chevy', 'chris', 'cream',
			'david', 'dirty', 'eagle', 'enjoy', 'enter', 'frank', 'girls', 'great', 'green',
			'happy', 'hello', 'horny', 'house', 'james', 'japan', 'jason', 'juice', 'kelly',
			'kevin', 'kitty', 'lover', 'lucky', 'magic', 'money', 'movie', 'music', 'naked',
			'ou812', 'paris', 'penis', 'peter', 'porno', 'power', 'pussy', 'qwert', 'sammy',
			'scott', 'smith', 'stars', 'steve', 'super', 'teens', 'tiger', 'video', 'viper',
			'white', 'women', 'xxxxx', 'young', '111111', '112233', '121212', '123123',
			'123456', '131313', '232323', '654321', '666666', '696969', '777777', '987654',
			'aaaaaa', 'abc123', 'abcdef', 'access', 'action', 'albert', 'alexis', 'amanda',
			'andrea', 'andrew', 'angela', 'angels', 'animal', 'apollo', 'apples', 'arthur',
			'asdfgh', 'ashley', 'august', 'austin', 'badboy', 'bailey', 'banana', 'barney',
			'batman', 'beaver', 'beavis', 'bigdog', 'birdie', 'biteme', 'blazer', 'blonde',
			'blowme', 'bonnie', 'booboo', 'booger', 'boomer', 'boston', 'brandy', 'braves',
			'brazil', 'bronco', 'buster', 'butter', 'calvin', 'camaro', 'canada', 'carlos',
			'carter', 'casper', 'cheese', 'coffee', 'compaq', 'cookie', 'cooper', 'cowboy',
			'dakota', 'dallas', 'daniel', 'debbie', 'dennis', 'diablo', 'doctor', 'doggie',
			'donald', 'dragon', 'dreams', 'driver', 'eagle1', 'eagles', 'edward', 'erotic',
			'falcon', 'fender', 'flower', 'flyers', 'freddy', 'fucked', 'fucker', 'fuckme',
			'gators', 'gemini', 'george', 'giants', 'ginger', 'golden', 'golfer', 'gordon',
			'guitar', 'gunner', 'hammer', 'hannah', 'harley', 'helpme', 'hentai', 'hockey',
			'horney', 'hotdog', 'hunter', 'iceman', 'iwantu', 'jackie', 'jaguar', 'jasper',
			'jeremy', 'johnny', 'jordan', 'joseph', 'joshua', 'junior', 'justin', 'killer',
			'knight', 'ladies', 'lakers', 'lauren', 'legend', 'little', 'london', 'lovers',
			'maddog', 'maggie', 'magnum', 'marine', 'martin', 'marvin', 'master', 'matrix',
			'member', 'merlin', 'mickey', 'miller', 'monica', 'monkey', 'morgan', 'mother',
			'muffin', 'murphy', 'nascar', 'nathan', 'nicole', 'nipple', 'oliver', 'orange',
			'parker', 'peanut', 'pepper', 'player', 'please', 'pookie', 'prince', 'purple',
			'qazwsx', 'qwerty', 'rabbit', 'rachel', 'racing', 'ranger', 'redsox', 'robert',
			'rocket', 'runner', 'russia', 'samson', 'sandra', 'saturn', 'scooby', 'secret',
			'sexsex', 'shadow', 'shaved', 'sierra', 'silver', 'skippy', 'slayer', 'smokey',
			'snoopy', 'soccer', 'sophie', 'spanky', 'sparky', 'spider', 'squirt', 'steven',
			'sticky', 'stupid', 'suckit', 'summer', 'surfer', 'sydney', 'taylor', 'tennis',
			'teresa', 'tester', 'theman', 'thomas', 'tigers', 'tigger', 'tomcat', 'topgun',
			'toyota', 'travis', 'tucker', 'turtle', 'united', 'vagina', 'victor', 'viking',
			'voodoo', 'walter', 'willie', 'wilson', 'winner', 'winter', 'wizard', 'xavier',
			'xxxxxx', 'yamaha', 'yankee', 'yellow', 'zxcvbn', 'zzzzzz', '1234567', '7777777',
			'8675309', 'abgrtyu', 'amateur', 'anthony', 'arsenal', 'asshole', 'bigcock',
			'bigdick', 'bigtits', 'bitches', 'blondes', 'blowjob', 'bond007', 'brandon',
			'broncos', 'bulldog', 'cameron', 'captain', 'charles', 'charlie', 'chelsea',
			'chester', 'chicago', 'chicken', 'college', 'cowboys', 'crystal', 'cumming',
			'cumshot', 'diamond', 'dolphin', 'extreme', 'ferrari', 'fishing', 'florida',
			'forever', 'freedom', 'fucking', 'fuckyou', 'gandalf', 'gateway', 'gregory',
			'heather', 'hooters', 'hunting', 'jackson', 'jasmine', 'jessica', 'johnson',
			'leather', 'letmein', 'madison', 'matthew', 'maxwell', 'melissa', 'michael',
			'monster', 'mustang', 'naughty', 'ncc1701', 'newyork', 'nipples', 'packers',
			'panther', 'panties', 'patrick', 'peaches', 'phantom', 'phoenix', 'porsche',
			'private', 'pussies', 'raiders', 'rainbow', 'rangers', 'rebecca', 'richard',
			'rosebud', 'scooter', 'scorpio', 'shannon', 'success', 'testing', 'thunder',
			'thx1138', 'tiffany', 'trouble', 'twitter', 'voyager', 'warrior', 'welcome',
			'william', 'winston', 'yankees', 'zxcvbnm', '11111111', '12345678', 'access14',
			'baseball', 'bigdaddy', 'butthead', 'cocacola', 'computer', 'corvette',
			'danielle', 'dolphins', 'einstein', 'firebird', 'football', 'hardcore',
			'iloveyou', 'internet', 'jennifer', 'marlboro', 'maverick', 'mercedes',
			'michelle', 'midnight', 'mistress', 'mountain', 'nicholas', 'password',
			'princess', 'qwertyui', 'redskins', 'redwings', 'rush2112', 'samantha',
			'scorpion', 'srinivas', 'startrek', 'starwars', 'steelers', 'sunshine',
			'superman', 'swimming', 'trustno1', 'victoria', 'whatever', 'xxxxxxxx',
			'password1', 'password12', 'password123');

	private $minimumChars = 8;

	private $strengthScaleFactor = 1;

	private $bannedPasswords = array();

	private $banMode = 'strict'; // (strict|loose)

	private $encoding = 'UTF-8';

	/**
	 * Constructor
	 *
	 * @param   array  $options  Override default options using an associative array of options
	 *
	 * Options:
	 *  - minimumChars: Minimum password length (default: 8)
	 *  - strengthScaleFactor: Required password strength multiplier (default: 1)
	 *  - bannedPasswords: Custom list of banned passwords (default: long list of common passwords)
	 *  - banMode: Use strict or loose comparisons for banned passwords. "strict" = don't allow a substring of a banned
	 *  password, "loose" = only ban exact matches (default: strict)
	 *  - encoding: Character set encoding of the password (default: UTF-8)
	 */
	public function __construct(array $options = array())
	{
		$this->bannedPasswords = self::$BANLIST;

		foreach ($options as $opt => $val)
		{
			if ($opt === 'banmode')
			{
				trigger_error('The lowercase banmode option is deprecated. Use banMode instead.', E_USER_DEPRECATED);
				$opt = 'banMode';
			}

			$this->{$opt} = $val;
		}
	}

	/**
	 * Determine the complexity added from a character set if it is used in a string
	 *
	 * @param   string $str String to check
	 * @param          int  [2]    $charset  Array of unicode code points representing the lower and upper bound of the
	 *                             character range
	 *
	 * @return  int  0 if there are no characters from the character set, size of the character set if there are any
	 *               characters used in the string
	 */
	private function additionalComplexityForCharset($str, $charset)
	{
		$len = mb_strlen($str, $this->encoding);
		for ($i = 0; $i < $len; $i++)
		{
			$c =
					unpack('Nord', mb_convert_encoding(mb_substr($str, $i, 1, $this->encoding), 'UCS-4BE', $this->encoding));
			if ($charset[0] <= $c['ord'] && $c['ord'] <= $charset[1])
			{
				return $charset[1] - $charset[0] + 1;
			}
		}

		return 0;
	}

	/**
	 * Check if a string is in the banned password list
	 *
	 * @param  string $str String to check
	 *
	 * @return  bool  TRUE if $str is a banned password, or if it is a substring of a banned password and
	 *                $this->banMode is 'strict'
	 */
	private function inBanlist($str)
	{
		if ($str == '')
		{
			return false;
		}

		$str = mb_convert_case($str, MB_CASE_LOWER, $this->encoding);

		if ($this->banMode === 'strict')
		{
			for ($i = 0; $i < count($this->bannedPasswords); $i++)
			{
				if (mb_strpos($this->bannedPasswords[ $i ], $str, 0, $this->encoding) !== false)
				{
					return true;
				}
			}

			return false;
		}

		return in_array($str, $this->bannedPasswords);
	}

	/**
	 * Check the complexity of a password
	 *
	 * @param  string $password The password to check
	 *
	 * @return  object  StdClass object with properties "valid", "complexity", and "error"
	 *  - valid: TRUE if the password is complex enough, FALSE if it is not
	 *  - complexity: The complexity of the password as a percent
	 *  - errors: Array containing descriptions of what made the password fail. Possible values are: banned, toosimple,
	 *  tooshort
	 */
	public function evaluateSecurity($password)
	{
		$complexity = 0;
		$error      = array();

		// Reset complexity to 0 when banned password is found
		if (!$this->inBanlist($password))
		{
			// Add character complexity
			foreach (self::$CHARSETS as $charset)
			{
				$complexity += $this->additionalComplexityForCharset($password, $charset);
			}
		}
		else
		{
			array_push($error, 'banned');
			$complexity = 1;
		}

		// Use natural log to produce linear scale
		$complexity = log(pow($complexity, mb_strlen($password, $this->encoding))) * (1 / $this->strengthScaleFactor);

		if ($complexity <= self::$MIN_COMPLEXITY)
		{
			array_push($error, 'toosimple');
		}

		if (mb_strlen($password, $this->encoding) < $this->minimumChars)
		{
			array_push($error, 'tooshort');
		}

		// Scale to percentage, so it can be used for a progress bar
		$complexity = ($complexity / self::$MAX_COMPLEXITY) * 100;
		$complexity = ($complexity > 100) ? 100 : $complexity;

		return (object)array('valid' => count($error) === 0, 'complexity' => $complexity, 'errors' => $error);
	}

	/**
	 * Checks if a password is strong enough for use on a live site. Used to check the front-end Secret Word.
	 *
	 * @param   string  $password         The password to check
	 * @param   bool    $throwExceptions  Throw an exception if the password is not strong enough?
	 *
	 * @return  bool
	 */
	public static function isStrongEnough($password, $throwExceptions = true)
	{
		$complexify = new self();

		$res = (object) array(
			'valid' => strlen($password) >= 32,
			'complexity' => 50,
			'errors' => (strlen($password) >= 32) ? array() : array('tooshort')
		);

		if (function_exists('mb_strlen') && function_exists('mb_convert_encoding') &&
			function_exists('mb_substr') && function_exists('mb_convert_case'))
		{
			$res = $complexify->evaluateSecurity($password);
		}


		if ($res->valid)
		{
			return true;
		}

		if (!$throwExceptions)
		{
			return false;
		}

		$error = count($res->errors) ? array_shift($res->errors) : 'toosimple';

		$errorMessage = Platform::getInstance()->translate('COM_AKEEBA_CPANEL_ERR_FESECRETWORD_' . $error);

		throw new \RuntimeException($errorMessage, 403);
	}
}
