Hashids.php 11 KB

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