diff --git a/composer.json b/composer.json index 199fd9f7..b7725614 100644 --- a/composer.json +++ b/composer.json @@ -48,7 +48,6 @@ "mexitek/phpcolors": "^1.0.4", "phpdocumentor/reflection-docblock": "^5.3.0", "stripe/stripe-php": "^17.4.0", - "hashids/hashids": "^4.1.0", "rakit/validation": "dev-master#ff003a35cdf5030a5f2482299f4c93f344a35b29", "ifsnop/mysqldump-php": "^2.12", "mpdf/mpdf": "^8.2.0", diff --git a/composer.lock b/composer.lock index 7ca75262..bfcdcda1 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "9e987490a8c309b5c05621809a19d7fa", + "content-hash": "3708aee0fdd43843d833791f07706f53", "packages": [ { "name": "amphp/amp", @@ -1577,76 +1577,6 @@ ], "time": "2025-08-23T21:21:41+00:00" }, - { - "name": "hashids/hashids", - "version": "4.1.0", - "source": { - "type": "git", - "url": "https://github.com/vinkla/hashids.git", - "reference": "8cab111f78e0bd9c76953b082919fc9e251761be" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/vinkla/hashids/zipball/8cab111f78e0bd9c76953b082919fc9e251761be", - "reference": "8cab111f78e0bd9c76953b082919fc9e251761be", - "shasum": "" - }, - "require": { - "ext-mbstring": "*", - "php": "^7.2 || ^8.0" - }, - "require-dev": { - "phpunit/phpunit": "^8.0 || ^9.4", - "squizlabs/php_codesniffer": "^3.5" - }, - "suggest": { - "ext-bcmath": "Required to use BC Math arbitrary precision mathematics (*).", - "ext-gmp": "Required to use GNU multiple precision mathematics (*)." - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.1-dev" - } - }, - "autoload": { - "psr-4": { - "Hashids\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Ivan Akimov", - "email": "ivan@barreleye.com" - }, - { - "name": "Vincent Klaiber", - "email": "hello@doubledip.se" - } - ], - "description": "Generate short, unique, non-sequential ids (like YouTube and Bitly) from numbers", - "homepage": "https://hashids.org/php", - "keywords": [ - "bitly", - "decode", - "encode", - "hash", - "hashid", - "hashids", - "ids", - "obfuscate", - "youtube" - ], - "support": { - "issues": "https://github.com/vinkla/hashids/issues", - "source": "https://github.com/vinkla/hashids/tree/4.1.0" - }, - "time": "2020-11-26T19:24:33+00:00" - }, { "name": "ifsnop/mysqldump-php", "version": "v2.12", diff --git a/inc/helpers/class-hash.php b/inc/helpers/class-hash.php index 4582c565..a917b4fd 100644 --- a/inc/helpers/class-hash.php +++ b/inc/helpers/class-hash.php @@ -2,6 +2,8 @@ /** * Handles hashing to encode ids and prevent spoofing due to auto-increments. * + * Uses a pure PHP implementation with no external dependencies. + * * @package WP_Ultimo * @subpackage Helper * @since 2.0.0 @@ -9,8 +11,6 @@ namespace WP_Ultimo\Helpers; -use Hashids\Hashids; - // Exit if accessed directly defined('ABSPATH') || exit; @@ -26,6 +26,11 @@ class Hash { */ const LENGTH = 10; + /** + * The character set used for encoding. + */ + const ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890'; + /** * Static-only class. */ @@ -42,9 +47,26 @@ private function __construct() {} */ public static function encode($number, $group = 'wp-ultimo') { - $hasher = new Hashids($group, self::LENGTH, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890'); + $alphabet = self::shuffle_alphabet($group); + $base = strlen($alphabet); + $seed = self::derive_seed($group); + + $obfuscated = (int) $number ^ $seed; + + // Ensure positive value for base conversion. + if ($obfuscated < 0) { + $obfuscated = ~$obfuscated; + } + + $result = ''; + + do { + $result = $alphabet[ $obfuscated % $base ] . $result; - return $hasher->encode($number); + $obfuscated = intdiv($obfuscated, $base); + } while ($obfuscated > 0); + + return str_pad($result, self::LENGTH, $alphabet[0], STR_PAD_LEFT); } /** @@ -58,8 +80,64 @@ public static function encode($number, $group = 'wp-ultimo') { */ public static function decode($hash, $group = 'wp-ultimo') { - $hasher = new Hashids($group, 10, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890'); + if (empty($hash) || ! is_string($hash)) { + return false; + } + + $alphabet = self::shuffle_alphabet($group); + $base = strlen($alphabet); + $seed = self::derive_seed($group); + + $number = 0; + $hash_length = strlen($hash); + + for ($i = 0; $i < $hash_length; $i++) { + $pos = strpos($alphabet, $hash[ $i ]); + + if (false === $pos) { + return false; + } + + $number = $number * $base + $pos; + } + + return $number ^ $seed; + } + + /** + * Creates a deterministic shuffled alphabet based on the group string. + * + * Uses md5 as a portable source of deterministic pseudo-random bytes. + * + * @since 2.5.0 + * + * @param string $group The group/salt string. + * @return string The shuffled alphabet. + */ + private static function shuffle_alphabet(string $group): string { + + $chars = str_split(self::ALPHABET); + $key = md5($group); + + for ($i = count($chars) - 1; $i > 0; $i--) { + $j = ord($key[ $i % strlen($key) ]) % ($i + 1); + + [$chars[ $i ], $chars[ $j ]] = [$chars[ $j ], $chars[ $i ]]; + } + + return implode('', $chars); + } + + /** + * Derives a positive numeric seed from the group string. + * + * @since 2.5.0 + * + * @param string $group The group/salt string. + * @return int A positive integer seed. + */ + private static function derive_seed(string $group): int { - return current($hasher->decode($hash)); + return abs(crc32($group . ':wu-hash-seed')); } }