|
@@ -0,0 +1,422 @@
|
|
|
+<?php
|
|
|
+
|
|
|
+/**
|
|
|
+ * Copyright (c) Ivan Akimov.
|
|
|
+ *
|
|
|
+ * For the full copyright and license information, please view the LICENSE
|
|
|
+ * file that was distributed with this source code.
|
|
|
+ *
|
|
|
+ * @see https://github.com/vinkla/hashids
|
|
|
+ */
|
|
|
+
|
|
|
+namespace app\utils\Hashids;
|
|
|
+
|
|
|
+use app\utils\Hashids\Math\Bc;
|
|
|
+use app\utils\Hashids\Math\Gmp;
|
|
|
+use app\utils\Hashids\Math\MathInterface;
|
|
|
+use RuntimeException;
|
|
|
+
|
|
|
+class Hashids implements HashidsInterface
|
|
|
+{
|
|
|
+ /**
|
|
|
+ * The seps divider.
|
|
|
+ *
|
|
|
+ * @var float
|
|
|
+ */
|
|
|
+ public const SEP_DIV = 3.5;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * The guard divider.
|
|
|
+ *
|
|
|
+ * @var float
|
|
|
+ */
|
|
|
+ public const GUARD_DIV = 12;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * The alphabet string.
|
|
|
+ *
|
|
|
+ * @var string
|
|
|
+ */
|
|
|
+ protected $alphabet;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Shuffled alphabets, referenced by alphabet and salt.
|
|
|
+ *
|
|
|
+ * @var array
|
|
|
+ */
|
|
|
+ protected $shuffledAlphabets;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * The seps string.
|
|
|
+ *
|
|
|
+ * @var string
|
|
|
+ */
|
|
|
+ protected $seps = 'cfhistuCFHISTU';
|
|
|
+
|
|
|
+ /**
|
|
|
+ * The guards string.
|
|
|
+ *
|
|
|
+ * @var string
|
|
|
+ */
|
|
|
+ protected $guards;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * The minimum hash length.
|
|
|
+ *
|
|
|
+ * @var int
|
|
|
+ */
|
|
|
+ protected $minHashLength;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * The salt string.
|
|
|
+ *
|
|
|
+ * @var string
|
|
|
+ */
|
|
|
+ protected $salt;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * The math class.
|
|
|
+ *
|
|
|
+ * @var \Hashids\Math\MathInterface
|
|
|
+ */
|
|
|
+ protected $math;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Create a new hashids instance.
|
|
|
+ *
|
|
|
+ * @param string $salt
|
|
|
+ * @param int $minHashLength
|
|
|
+ * @param string $alphabet
|
|
|
+ *
|
|
|
+ * @throws \Hashids\HashidsException
|
|
|
+ */
|
|
|
+ public function __construct($salt = '', $minHashLength = 0, $alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890')
|
|
|
+ {
|
|
|
+ $this->salt = \mb_convert_encoding($salt, 'UTF-8', \mb_detect_encoding($salt));
|
|
|
+ $this->minHashLength = $minHashLength;
|
|
|
+ $alphabet = \mb_convert_encoding($alphabet, 'UTF-8', \mb_detect_encoding($alphabet));
|
|
|
+ $this->alphabet = \implode('', \array_unique($this->multiByteSplit($alphabet)));
|
|
|
+ $this->math = $this->getMathExtension();
|
|
|
+
|
|
|
+ if (\mb_strlen($this->alphabet) < 16) {
|
|
|
+ throw new HashidsException('Alphabet must contain at least 16 unique characters.');
|
|
|
+ }
|
|
|
+
|
|
|
+ if (false !== \mb_strpos($this->alphabet, ' ')) {
|
|
|
+ throw new HashidsException('Alphabet can\'t contain spaces.');
|
|
|
+ }
|
|
|
+
|
|
|
+ $alphabetArray = $this->multiByteSplit($this->alphabet);
|
|
|
+ $sepsArray = $this->multiByteSplit($this->seps);
|
|
|
+ $this->seps = \implode('', \array_intersect($sepsArray, $alphabetArray));
|
|
|
+ $this->alphabet = \implode('', \array_diff($alphabetArray, $sepsArray));
|
|
|
+ $this->seps = $this->shuffle($this->seps, $this->salt);
|
|
|
+
|
|
|
+ if (!$this->seps || (\mb_strlen($this->alphabet) / \mb_strlen($this->seps)) > self::SEP_DIV) {
|
|
|
+ $sepsLength = (int) \ceil(\mb_strlen($this->alphabet) / self::SEP_DIV);
|
|
|
+
|
|
|
+ if ($sepsLength > \mb_strlen($this->seps)) {
|
|
|
+ $diff = $sepsLength - \mb_strlen($this->seps);
|
|
|
+ $this->seps .= \mb_substr($this->alphabet, 0, $diff);
|
|
|
+ $this->alphabet = \mb_substr($this->alphabet, $diff);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ $this->alphabet = $this->shuffle($this->alphabet, $this->salt);
|
|
|
+ $guardCount = (int) \ceil(\mb_strlen($this->alphabet) / self::GUARD_DIV);
|
|
|
+
|
|
|
+ if (\mb_strlen($this->alphabet) < 3) {
|
|
|
+ $this->guards = \mb_substr($this->seps, 0, $guardCount);
|
|
|
+ $this->seps = \mb_substr($this->seps, $guardCount);
|
|
|
+ } else {
|
|
|
+ $this->guards = \mb_substr($this->alphabet, 0, $guardCount);
|
|
|
+ $this->alphabet = \mb_substr($this->alphabet, $guardCount);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Encode parameters to generate a hash.
|
|
|
+ *
|
|
|
+ * @param mixed $numbers
|
|
|
+ *
|
|
|
+ * @return string
|
|
|
+ */
|
|
|
+ public function encode(...$numbers): string
|
|
|
+ {
|
|
|
+ $ret = '';
|
|
|
+
|
|
|
+ if (1 === \count($numbers) && \is_array($numbers[0])) {
|
|
|
+ $numbers = $numbers[0];
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!$numbers) {
|
|
|
+ return $ret;
|
|
|
+ }
|
|
|
+
|
|
|
+ foreach ($numbers as $number) {
|
|
|
+ $isNumber = \ctype_digit((string) $number);
|
|
|
+
|
|
|
+ if (!$isNumber) {
|
|
|
+ return $ret;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ $alphabet = $this->alphabet;
|
|
|
+ $numbersSize = \count($numbers);
|
|
|
+ $numbersHashInt = 0;
|
|
|
+
|
|
|
+ foreach ($numbers as $i => $number) {
|
|
|
+ $numbersHashInt += $this->math->intval($this->math->mod($number, $i + 100));
|
|
|
+ }
|
|
|
+
|
|
|
+ $lottery = $ret = \mb_substr($alphabet, $numbersHashInt % \mb_strlen($alphabet), 1);
|
|
|
+ foreach ($numbers as $i => $number) {
|
|
|
+ $alphabet = $this->shuffle($alphabet, \mb_substr($lottery . $this->salt . $alphabet, 0, \mb_strlen($alphabet)));
|
|
|
+ $ret .= $last = $this->hash($number, $alphabet);
|
|
|
+
|
|
|
+ if ($i + 1 < $numbersSize) {
|
|
|
+ $number %= (\mb_ord($last, 'UTF-8') + $i);
|
|
|
+ $sepsIndex = $this->math->intval($this->math->mod($number, \mb_strlen($this->seps)));
|
|
|
+ $ret .= \mb_substr($this->seps, $sepsIndex, 1);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (\mb_strlen($ret) < $this->minHashLength) {
|
|
|
+ $guardIndex = ($numbersHashInt + \mb_ord(\mb_substr($ret, 0, 1), 'UTF-8')) % \mb_strlen($this->guards);
|
|
|
+
|
|
|
+ $guard = \mb_substr($this->guards, $guardIndex, 1);
|
|
|
+ $ret = $guard . $ret;
|
|
|
+
|
|
|
+ if (\mb_strlen($ret) < $this->minHashLength) {
|
|
|
+ $guardIndex = ($numbersHashInt + \mb_ord(\mb_substr($ret, 2, 1), 'UTF-8')) % \mb_strlen($this->guards);
|
|
|
+ $guard = \mb_substr($this->guards, $guardIndex, 1);
|
|
|
+
|
|
|
+ $ret .= $guard;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ $halfLength = (int) (\mb_strlen($alphabet) / 2);
|
|
|
+ while (\mb_strlen($ret) < $this->minHashLength) {
|
|
|
+ $alphabet = $this->shuffle($alphabet, $alphabet);
|
|
|
+ $ret = \mb_substr($alphabet, $halfLength) . $ret . \mb_substr($alphabet, 0, $halfLength);
|
|
|
+
|
|
|
+ $excess = \mb_strlen($ret) - $this->minHashLength;
|
|
|
+ if ($excess > 0) {
|
|
|
+ $ret = \mb_substr($ret, (int) ($excess / 2), $this->minHashLength);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return $ret;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Decode a hash to the original parameter values.
|
|
|
+ *
|
|
|
+ * @param string $hash
|
|
|
+ *
|
|
|
+ * @return array
|
|
|
+ */
|
|
|
+ public function decode($hash): array
|
|
|
+ {
|
|
|
+ $ret = [];
|
|
|
+
|
|
|
+ if (!\is_string($hash) || !($hash = \trim($hash))) {
|
|
|
+ return $ret;
|
|
|
+ }
|
|
|
+
|
|
|
+ $alphabet = $this->alphabet;
|
|
|
+
|
|
|
+ $hashBreakdown = \str_replace($this->multiByteSplit($this->guards), ' ', $hash);
|
|
|
+ $hashArray = \explode(' ', $hashBreakdown);
|
|
|
+
|
|
|
+ $i = 3 === \count($hashArray) || 2 === \count($hashArray) ? 1 : 0;
|
|
|
+
|
|
|
+ $hashBreakdown = $hashArray[$i];
|
|
|
+
|
|
|
+ if ('' !== $hashBreakdown) {
|
|
|
+ $lottery = \mb_substr($hashBreakdown, 0, 1);
|
|
|
+ $hashBreakdown = \mb_substr($hashBreakdown, 1);
|
|
|
+
|
|
|
+ $hashBreakdown = \str_replace($this->multiByteSplit($this->seps), ' ', $hashBreakdown);
|
|
|
+ $hashArray = \explode(' ', $hashBreakdown);
|
|
|
+
|
|
|
+ foreach ($hashArray as $subHash) {
|
|
|
+ $alphabet = $this->shuffle($alphabet, \mb_substr($lottery . $this->salt . $alphabet, 0, \mb_strlen($alphabet)));
|
|
|
+ $result = $this->unhash($subHash, $alphabet);
|
|
|
+ if ($this->math->greaterThan($result, PHP_INT_MAX)) {
|
|
|
+ $ret[] = $this->math->strval($result);
|
|
|
+ } else {
|
|
|
+ $ret[] = $this->math->intval($result);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if ($this->encode($ret) !== $hash) {
|
|
|
+ $ret = [];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return $ret;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Encode hexadecimal values and generate a hash string.
|
|
|
+ *
|
|
|
+ * @param string $str
|
|
|
+ *
|
|
|
+ * @return string
|
|
|
+ */
|
|
|
+ public function encodeHex($str): string
|
|
|
+ {
|
|
|
+ if (!\ctype_xdigit((string) $str)) {
|
|
|
+ return '';
|
|
|
+ }
|
|
|
+
|
|
|
+ $numbers = \trim(chunk_split($str, 12, ' '));
|
|
|
+ $numbers = \explode(' ', $numbers);
|
|
|
+
|
|
|
+ foreach ($numbers as $i => $number) {
|
|
|
+ $numbers[$i] = \hexdec('1' . $number);
|
|
|
+ }
|
|
|
+
|
|
|
+ return $this->encode(...$numbers);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Decode a hexadecimal hash.
|
|
|
+ *
|
|
|
+ * @param string $hash
|
|
|
+ *
|
|
|
+ * @return string
|
|
|
+ */
|
|
|
+ public function decodeHex($hash): string
|
|
|
+ {
|
|
|
+ $ret = '';
|
|
|
+ $numbers = $this->decode($hash);
|
|
|
+
|
|
|
+ foreach ($numbers as $i => $number) {
|
|
|
+ $ret .= \mb_substr(dechex($number), 1);
|
|
|
+ }
|
|
|
+
|
|
|
+ return $ret;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Shuffle alphabet by given salt.
|
|
|
+ *
|
|
|
+ * @param string $alphabet
|
|
|
+ * @param string $salt
|
|
|
+ *
|
|
|
+ * @return string
|
|
|
+ */
|
|
|
+ protected function shuffle($alphabet, $salt): string
|
|
|
+ {
|
|
|
+ $key = $alphabet . ' ' . $salt;
|
|
|
+
|
|
|
+ if (isset($this->shuffledAlphabets[$key])) {
|
|
|
+ return $this->shuffledAlphabets[$key];
|
|
|
+ }
|
|
|
+
|
|
|
+ $saltLength = \mb_strlen($salt);
|
|
|
+ $saltArray = $this->multiByteSplit($salt);
|
|
|
+ if (!$saltLength) {
|
|
|
+ return $alphabet;
|
|
|
+ }
|
|
|
+ $alphabetArray = $this->multiByteSplit($alphabet);
|
|
|
+ for ($i = \mb_strlen($alphabet) - 1, $v = 0, $p = 0; $i > 0; $i--, $v++) {
|
|
|
+ $v %= $saltLength;
|
|
|
+ $p += $int = \mb_ord($saltArray[$v], 'UTF-8');
|
|
|
+ $j = ($int + $v + $p) % $i;
|
|
|
+
|
|
|
+ $temp = $alphabetArray[$j];
|
|
|
+ $alphabetArray[$j] = $alphabetArray[$i];
|
|
|
+ $alphabetArray[$i] = $temp;
|
|
|
+ }
|
|
|
+ $alphabet = \implode('', $alphabetArray);
|
|
|
+ $this->shuffledAlphabets[$key] = $alphabet;
|
|
|
+
|
|
|
+ return $alphabet;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Hash given input value.
|
|
|
+ *
|
|
|
+ * @param string $input
|
|
|
+ * @param string $alphabet
|
|
|
+ *
|
|
|
+ * @return string
|
|
|
+ */
|
|
|
+ protected function hash($input, $alphabet): string
|
|
|
+ {
|
|
|
+ $hash = '';
|
|
|
+ $alphabetLength = \mb_strlen($alphabet);
|
|
|
+
|
|
|
+ do {
|
|
|
+ $hash = \mb_substr($alphabet, $this->math->intval($this->math->mod($input, $alphabetLength)), 1) . $hash;
|
|
|
+
|
|
|
+ $input = $this->math->divide($input, $alphabetLength);
|
|
|
+ } while ($this->math->greaterThan($input, 0));
|
|
|
+
|
|
|
+ return $hash;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Unhash given input value.
|
|
|
+ *
|
|
|
+ * @param string $input
|
|
|
+ * @param string $alphabet
|
|
|
+ *
|
|
|
+ * @return int
|
|
|
+ */
|
|
|
+ protected function unhash($input, $alphabet)
|
|
|
+ {
|
|
|
+ $number = 0;
|
|
|
+ $inputLength = \mb_strlen($input);
|
|
|
+
|
|
|
+ if ($inputLength && $alphabet) {
|
|
|
+ $alphabetLength = \mb_strlen($alphabet);
|
|
|
+ $inputChars = $this->multiByteSplit($input);
|
|
|
+
|
|
|
+ foreach ($inputChars as $char) {
|
|
|
+ $position = \mb_strpos($alphabet, $char);
|
|
|
+ $number = $this->math->multiply($number, $alphabetLength);
|
|
|
+ $number = $this->math->add($number, $position);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return $number;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Get BC Math or GMP extension.
|
|
|
+ *
|
|
|
+ * @codeCoverageIgnore
|
|
|
+ *
|
|
|
+ * @throws \RuntimeException
|
|
|
+ *
|
|
|
+ * @return \Hashids\Math\MathInterface
|
|
|
+ */
|
|
|
+ protected function getMathExtension(): MathInterface
|
|
|
+ {
|
|
|
+ if (\extension_loaded('gmp')) {
|
|
|
+ return new Gmp();
|
|
|
+ }
|
|
|
+
|
|
|
+ if (\extension_loaded('bcmath')) {
|
|
|
+ return new Bc();
|
|
|
+ }
|
|
|
+
|
|
|
+ throw new RuntimeException('Missing BC Math or GMP extension.');
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Replace simple use of $this->multiByteSplit with multi byte string.
|
|
|
+ *
|
|
|
+ * @param string $string
|
|
|
+ *
|
|
|
+ * @return array<int, string>
|
|
|
+ */
|
|
|
+ protected function multiByteSplit($string): array
|
|
|
+ {
|
|
|
+ return \preg_split('/(?!^)(?=.)/u', $string) ?: [];
|
|
|
+ }
|
|
|
+}
|