| 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) ?: [];    }}
 |