123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429 |
- <?php
- /*
- * This file is part of Hashids.
- *
- * (c) Ivan Akimov <ivan@barreleye.com>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Hashids;
- use Hashids\Math\Bc;
- use Hashids\Math\Gmp;
- use Hashids\Math\MathInterface;
- use RuntimeException;
- /**
- * This is the hashids class.
- *
- * @author Ivan Akimov <ivan@barreleye.com>
- * @author Vincent Klaiber <hello@doubledip.se>
- * @author Johnson Page <jwpage@gmail.com>
- */
- class Hashids implements HashidsInterface
- {
- /**
- * The seps divider.
- *
- * @var float
- */
- const SEP_DIV = 3.5;
- /**
- * The guard divider.
- *
- * @var float
- */
- 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
- *
- * @return array|string[]
- */
- protected function multiByteSplit($string): array
- {
- return \preg_split('/(?!^)(?=.)/u', $string) ?: [];
- }
- }
|