You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

430 lines
12 KiB

  1. <?php
  2. /*
  3. * This file is part of Hashids.
  4. *
  5. * (c) Ivan Akimov <ivan@barreleye.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Hashids;
  11. use Hashids\Math\Bc;
  12. use Hashids\Math\Gmp;
  13. use Hashids\Math\MathInterface;
  14. use RuntimeException;
  15. /**
  16. * This is the hashids class.
  17. *
  18. * @author Ivan Akimov <ivan@barreleye.com>
  19. * @author Vincent Klaiber <hello@doubledip.se>
  20. * @author Johnson Page <jwpage@gmail.com>
  21. */
  22. class Hashids implements HashidsInterface
  23. {
  24. /**
  25. * The seps divider.
  26. *
  27. * @var float
  28. */
  29. const SEP_DIV = 3.5;
  30. /**
  31. * The guard divider.
  32. *
  33. * @var float
  34. */
  35. const GUARD_DIV = 12;
  36. /**
  37. * The alphabet string.
  38. *
  39. * @var string
  40. */
  41. protected $alphabet;
  42. /**
  43. * Shuffled alphabets, referenced by alphabet and salt.
  44. *
  45. * @var array
  46. */
  47. protected $shuffledAlphabets;
  48. /**
  49. * The seps string.
  50. *
  51. * @var string
  52. */
  53. protected $seps = 'cfhistuCFHISTU';
  54. /**
  55. * The guards string.
  56. *
  57. * @var string
  58. */
  59. protected $guards;
  60. /**
  61. * The minimum hash length.
  62. *
  63. * @var int
  64. */
  65. protected $minHashLength;
  66. /**
  67. * The salt string.
  68. *
  69. * @var string
  70. */
  71. protected $salt;
  72. /**
  73. * The math class.
  74. *
  75. * @var \Hashids\Math\MathInterface
  76. */
  77. protected $math;
  78. /**
  79. * Create a new hashids instance.
  80. *
  81. * @param string $salt
  82. * @param int $minHashLength
  83. * @param string $alphabet
  84. *
  85. * @throws \Hashids\HashidsException
  86. */
  87. public function __construct($salt = '', $minHashLength = 0, $alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890')
  88. {
  89. $this->salt = \mb_convert_encoding($salt, 'UTF-8', \mb_detect_encoding($salt));
  90. $this->minHashLength = $minHashLength;
  91. $alphabet = \mb_convert_encoding($alphabet, 'UTF-8', \mb_detect_encoding($alphabet));
  92. $this->alphabet = \implode('', \array_unique($this->multiByteSplit($alphabet)));
  93. $this->math = $this->getMathExtension();
  94. if (\mb_strlen($this->alphabet) < 16) {
  95. throw new HashidsException('Alphabet must contain at least 16 unique characters.');
  96. }
  97. if (false !== \mb_strpos($this->alphabet, ' ')) {
  98. throw new HashidsException('Alphabet can\'t contain spaces.');
  99. }
  100. $alphabetArray = $this->multiByteSplit($this->alphabet);
  101. $sepsArray = $this->multiByteSplit($this->seps);
  102. $this->seps = \implode('', \array_intersect($sepsArray, $alphabetArray));
  103. $this->alphabet = \implode('', \array_diff($alphabetArray, $sepsArray));
  104. $this->seps = $this->shuffle($this->seps, $this->salt);
  105. if (!$this->seps || (\mb_strlen($this->alphabet) / \mb_strlen($this->seps)) > self::SEP_DIV) {
  106. $sepsLength = (int) \ceil(\mb_strlen($this->alphabet) / self::SEP_DIV);
  107. if ($sepsLength > \mb_strlen($this->seps)) {
  108. $diff = $sepsLength - \mb_strlen($this->seps);
  109. $this->seps .= \mb_substr($this->alphabet, 0, $diff);
  110. $this->alphabet = \mb_substr($this->alphabet, $diff);
  111. }
  112. }
  113. $this->alphabet = $this->shuffle($this->alphabet, $this->salt);
  114. $guardCount = (int) \ceil(\mb_strlen($this->alphabet) / self::GUARD_DIV);
  115. if (\mb_strlen($this->alphabet) < 3) {
  116. $this->guards = \mb_substr($this->seps, 0, $guardCount);
  117. $this->seps = \mb_substr($this->seps, $guardCount);
  118. } else {
  119. $this->guards = \mb_substr($this->alphabet, 0, $guardCount);
  120. $this->alphabet = \mb_substr($this->alphabet, $guardCount);
  121. }
  122. }
  123. /**
  124. * Encode parameters to generate a hash.
  125. *
  126. * @param mixed $numbers
  127. *
  128. * @return string
  129. */
  130. public function encode(...$numbers): string
  131. {
  132. $ret = '';
  133. if (1 === \count($numbers) && \is_array($numbers[0])) {
  134. $numbers = $numbers[0];
  135. }
  136. if (!$numbers) {
  137. return $ret;
  138. }
  139. foreach ($numbers as $number) {
  140. $isNumber = \ctype_digit((string) $number);
  141. if (!$isNumber) {
  142. return $ret;
  143. }
  144. }
  145. $alphabet = $this->alphabet;
  146. $numbersSize = \count($numbers);
  147. $numbersHashInt = 0;
  148. foreach ($numbers as $i => $number) {
  149. $numbersHashInt += $this->math->intval($this->math->mod($number, $i + 100));
  150. }
  151. $lottery = $ret = \mb_substr($alphabet, $numbersHashInt % \mb_strlen($alphabet), 1);
  152. foreach ($numbers as $i => $number) {
  153. $alphabet = $this->shuffle($alphabet, \mb_substr($lottery.$this->salt.$alphabet, 0, \mb_strlen($alphabet)));
  154. $ret .= $last = $this->hash($number, $alphabet);
  155. if ($i + 1 < $numbersSize) {
  156. $number %= (\mb_ord($last, 'UTF-8') + $i);
  157. $sepsIndex = $this->math->intval($this->math->mod($number, \mb_strlen($this->seps)));
  158. $ret .= \mb_substr($this->seps, $sepsIndex, 1);
  159. }
  160. }
  161. if (\mb_strlen($ret) < $this->minHashLength) {
  162. $guardIndex = ($numbersHashInt + \mb_ord(\mb_substr($ret, 0, 1), 'UTF-8')) % \mb_strlen($this->guards);
  163. $guard = \mb_substr($this->guards, $guardIndex, 1);
  164. $ret = $guard.$ret;
  165. if (\mb_strlen($ret) < $this->minHashLength) {
  166. $guardIndex = ($numbersHashInt + \mb_ord(\mb_substr($ret, 2, 1), 'UTF-8')) % \mb_strlen($this->guards);
  167. $guard = \mb_substr($this->guards, $guardIndex, 1);
  168. $ret .= $guard;
  169. }
  170. }
  171. $halfLength = (int) (\mb_strlen($alphabet) / 2);
  172. while (\mb_strlen($ret) < $this->minHashLength) {
  173. $alphabet = $this->shuffle($alphabet, $alphabet);
  174. $ret = \mb_substr($alphabet, $halfLength).$ret.\mb_substr($alphabet, 0, $halfLength);
  175. $excess = \mb_strlen($ret) - $this->minHashLength;
  176. if ($excess > 0) {
  177. $ret = \mb_substr($ret, (int) ($excess / 2), $this->minHashLength);
  178. }
  179. }
  180. return $ret;
  181. }
  182. /**
  183. * Decode a hash to the original parameter values.
  184. *
  185. * @param string $hash
  186. *
  187. * @return array
  188. */
  189. public function decode($hash): array
  190. {
  191. $ret = [];
  192. if (!\is_string($hash) || !($hash = \trim($hash))) {
  193. return $ret;
  194. }
  195. $alphabet = $this->alphabet;
  196. $hashBreakdown = \str_replace($this->multiByteSplit($this->guards), ' ', $hash);
  197. $hashArray = \explode(' ', $hashBreakdown);
  198. $i = 3 === \count($hashArray) || 2 === \count($hashArray) ? 1 : 0;
  199. $hashBreakdown = $hashArray[$i];
  200. if ('' !== $hashBreakdown) {
  201. $lottery = \mb_substr($hashBreakdown, 0, 1);
  202. $hashBreakdown = \mb_substr($hashBreakdown, 1);
  203. $hashBreakdown = \str_replace($this->multiByteSplit($this->seps), ' ', $hashBreakdown);
  204. $hashArray = \explode(' ', $hashBreakdown);
  205. foreach ($hashArray as $subHash) {
  206. $alphabet = $this->shuffle($alphabet, \mb_substr($lottery.$this->salt.$alphabet, 0, \mb_strlen($alphabet)));
  207. $result = $this->unhash($subHash, $alphabet);
  208. if ($this->math->greaterThan($result, PHP_INT_MAX)) {
  209. $ret[] = $this->math->strval($result);
  210. } else {
  211. $ret[] = $this->math->intval($result);
  212. }
  213. }
  214. if ($this->encode($ret) !== $hash) {
  215. $ret = [];
  216. }
  217. }
  218. return $ret;
  219. }
  220. /**
  221. * Encode hexadecimal values and generate a hash string.
  222. *
  223. * @param string $str
  224. *
  225. * @return string
  226. */
  227. public function encodeHex($str): string
  228. {
  229. if (!\ctype_xdigit((string) $str)) {
  230. return '';
  231. }
  232. $numbers = \trim(chunk_split($str, 12, ' '));
  233. $numbers = \explode(' ', $numbers);
  234. foreach ($numbers as $i => $number) {
  235. $numbers[$i] = \hexdec('1'.$number);
  236. }
  237. return $this->encode(...$numbers);
  238. }
  239. /**
  240. * Decode a hexadecimal hash.
  241. *
  242. * @param string $hash
  243. *
  244. * @return string
  245. */
  246. public function decodeHex($hash): string
  247. {
  248. $ret = '';
  249. $numbers = $this->decode($hash);
  250. foreach ($numbers as $i => $number) {
  251. $ret .= \mb_substr(dechex($number), 1);
  252. }
  253. return $ret;
  254. }
  255. /**
  256. * Shuffle alphabet by given salt.
  257. *
  258. * @param string $alphabet
  259. * @param string $salt
  260. *
  261. * @return string
  262. */
  263. protected function shuffle($alphabet, $salt): string
  264. {
  265. $key = $alphabet.' '.$salt;
  266. if (isset($this->shuffledAlphabets[$key])) {
  267. return $this->shuffledAlphabets[$key];
  268. }
  269. $saltLength = \mb_strlen($salt);
  270. $saltArray = $this->multiByteSplit($salt);
  271. if (!$saltLength) {
  272. return $alphabet;
  273. }
  274. $alphabetArray = $this->multiByteSplit($alphabet);
  275. for ($i = \mb_strlen($alphabet) - 1, $v = 0, $p = 0; $i > 0; $i--, $v++) {
  276. $v %= $saltLength;
  277. $p += $int = \mb_ord($saltArray[$v], 'UTF-8');
  278. $j = ($int + $v + $p) % $i;
  279. $temp = $alphabetArray[$j];
  280. $alphabetArray[$j] = $alphabetArray[$i];
  281. $alphabetArray[$i] = $temp;
  282. }
  283. $alphabet = \implode('', $alphabetArray);
  284. $this->shuffledAlphabets[$key] = $alphabet;
  285. return $alphabet;
  286. }
  287. /**
  288. * Hash given input value.
  289. *
  290. * @param string $input
  291. * @param string $alphabet
  292. *
  293. * @return string
  294. */
  295. protected function hash($input, $alphabet): string
  296. {
  297. $hash = '';
  298. $alphabetLength = \mb_strlen($alphabet);
  299. do {
  300. $hash = \mb_substr($alphabet, $this->math->intval($this->math->mod($input, $alphabetLength)), 1).$hash;
  301. $input = $this->math->divide($input, $alphabetLength);
  302. } while ($this->math->greaterThan($input, 0));
  303. return $hash;
  304. }
  305. /**
  306. * Unhash given input value.
  307. *
  308. * @param string $input
  309. * @param string $alphabet
  310. *
  311. * @return int
  312. */
  313. protected function unhash($input, $alphabet)
  314. {
  315. $number = 0;
  316. $inputLength = \mb_strlen($input);
  317. if ($inputLength && $alphabet) {
  318. $alphabetLength = \mb_strlen($alphabet);
  319. $inputChars = $this->multiByteSplit($input);
  320. foreach ($inputChars as $char) {
  321. $position = \mb_strpos($alphabet, $char);
  322. $number = $this->math->multiply($number, $alphabetLength);
  323. $number = $this->math->add($number, $position);
  324. }
  325. }
  326. return $number;
  327. }
  328. /**
  329. * Get BC Math or GMP extension.
  330. *
  331. * @codeCoverageIgnore
  332. *
  333. * @throws \RuntimeException
  334. *
  335. * @return \Hashids\Math\MathInterface
  336. */
  337. protected function getMathExtension(): MathInterface
  338. {
  339. if (\extension_loaded('gmp')) {
  340. return new Gmp();
  341. }
  342. if (\extension_loaded('bcmath')) {
  343. return new Bc();
  344. }
  345. throw new RuntimeException('Missing BC Math or GMP extension.');
  346. }
  347. /**
  348. * Replace simple use of $this->multiByteSplit with multi byte string.
  349. *
  350. * @param $string
  351. *
  352. * @return array|string[]
  353. */
  354. protected function multiByteSplit($string): array
  355. {
  356. return \preg_split('/(?!^)(?=.)/u', $string) ?: [];
  357. }
  358. }