/** Datei: plugins/cards/library/ARUNOX/Tools/Cardnumber/v2.php **/ * @copyright 2023 Sparky@ARUNOX * @version 1.0.0 * @since 1.0.0 File is created */ namespace ARUNOX\Tools\Cardnumber; use ARUNOX\Tools\Cardnumber; class v2 extends BaseCalc { /* ### CONSTANTS ################################################ # */ /* ### VARIABLES ################################################ # */ protected static int $NumberBase = 36; protected static bool $DoubleRemove = TRUE; protected static int $ChecksumBase = 36; protected static int $ChecksumLevel = 2; /* ### PUBLIC STATIC FUNCTIONS ################################## # */ /* ### PUBLIC FUNCTIONS ######################################### # */ /* ### PROTECTED FUNCTIONS ####################################### # */ protected function calcValue( string $input, null|int $sourceBase = NULL, null|int $targetBase = NULL ) : int { $input = preg_replace('/[^0-9A-Z]/i', '', $input ); $input = strtoupper( $input ); $input = substr( $input, 0 , $this->getNumberLength() + 8 ); $parts = str_split( $input ); $parts = array_map( fn( $part ) => $this->decode( $part, $sourceBase ), $parts ); $acc = 0; foreach( $parts as $part ) $acc = ( $acc * 31 + $part ); return (int)$acc; } protected function parseChecksum( int $acc, null|int $length = NULL, null|int $base = NULL ) : string { $base = $this->getChecksumBase( $base ); $level = $this->getChecksumLevel() - 1; $length = $this->getChecksumLength( $length ); $bucket = Cardnumber::getBucketSize(); $max = pow( $base, $length ); if( $bucket * ( $level + 1 ) > $max ) { throw new InvalidArgumentException('Bucket passt nicht in den verfügbaren Raum.'); } $acc = ( ($acc % $bucket) + $bucket ) % $bucket; $acc = $acc + $bucket * $level; $code = $this->encode( $acc, $base ); $code = str_pad( (string)$code, $length, '0', STR_PAD_LEFT ); $code = strtoupper( $code ); return $code; } protected function parseValidation( int $acc, null|int $length = NULL, null|int $base = NULL ) : string { $base = $this->getValidationBase( $base ); $length = $this->getValidationLength( $length ); $max = pow( $base, $length ); $acc = $acc % $max; $code = $this->encode( $acc, $base ); $code = str_pad( (string)$code, $length, '0', STR_PAD_LEFT ); $code = strtoupper( $code ); return $code; } /* ### PRIVATE FUNCTIONS ######################################## # */ /* ### MAGIC FUNCTIONS ########################################## # */ } /** Datei: plugins/cards/library/ARUNOX/Tools/Cardnumber/Cardnumber.php **/ * @copyright 2023 Sparky@ARUNOX * @version 1.0.0 * @since 1.0.0 File is created */ namespace ARUNOX\Tools; use InvalidArgumentException; class Cardnumber { /* ### CONSTANTS ################################################ # */ /* ### VARIABLES ################################################ # */ protected static int $bucketSize = 256; protected static array $levelClasses = []; protected static null|int $defaultLevel = NULL; /* ################################################################ # */ /* ### PUBLIC STATIC FUNCTIONS ################################## # */ /* ################################################################ # */ /* ### CHECK FUNCTIONS ########################################## # */ public static function check( string $input, null|string $serial = NULL ) : array { $result = [ 500, "INVALID", "UNKNOWN" ]; if( preg_match( '/(([a-z0-9]{4})([a-z0-9]{2}))([0-9]{3})?/i', $input, $matches ) ) { [ $input, $number, $uuid, $checksum, $validation ] = array_pad( $matches, 5, NULL ); $acc = base_convert( $checksum,36, 10 ); $size = static::getBucketSize(); $level = ceil( $acc / $size ); $tool = static::getLevelObject( $level ); if( $tool ) { $result = $tool->check( $input, $serial ); } array_push( $result, $number ); } return $result; } /* ### BUCKET FUNCTIONS ######################################### # */ public static function setBucketSize( int $size ) : void { static::$bucketSize = $size; } public static function getBucketSize() : int { return static::$bucketSize; } /* ### DEFAULT FUNCTIONS ######################################## # */ public static function setDefaultLevel( int $level ) : bool { if( static::getLevelClass( $level ) ) { static::$defaultLevel = $level; return TRUE; } return FALSE; } public static function getDefaultLevel() : null|int { return static::$defaultLevel; } public static function getDefaultClass() : bool|string { if( $class = static::getLevelClass( static::$defaultLevel ) ) { return $class; } return FALSE; } public static function getDefaultObject() : bool|object { if( $class = static::getLevelClass( static::$defaultLevel ) ) { return new $class(); } return FALSE; } /* ### LEVEL FUNCTIONS ########################################## # */ public static function setLevelClass( int $level, string $class ) : bool { if( class_exists( $class ) ) { static::$levelClasses[ $level ] = $class; return TRUE; } return FALSE; } public static function getLevelClass( int $level ) : bool|string { if( key_exists( $level, static::$levelClasses ) ) { return static::$levelClasses[ $level ]; } return FALSE; } public static function getLevelObject( int $level ) : bool|object { if( $class = static::getLevelClass( $level ) ) { return new $class(); } return FALSE; } /* ### PUBLIC FUNCTIONS ######################################### # */ /* ### PROTECTED FUNCTIONS ####################################### # */ /* ### PRIVATE FUNCTIONS ######################################## # */ /* ### MAGIC FUNCTIONS ########################################## # */ public function __call( string $name, $args ) { } public static function __callStatic( string $name, $args ) { } } /** Datei: plugins/cards/library/ARUNOX/Tools/Cardnumber/v1.php **/ * @copyright 2023 Sparky@ARUNOX * @version 1.0.0 * @since 1.0.0 File is created */ namespace ARUNOX\Tools\Cardnumber; class v1 extends BaseCalc { /* ### CONSTANTS ################################################ # */ /* ### VARIABLES ################################################ # */ protected static int $ChecksumBase = 16; protected static int $ChecksumLevel = 1; protected static int $NumberBase = 16; /* ### PUBLIC STATIC FUNCTIONS ################################## # */ /* ### PUBLIC FUNCTIONS ######################################### # */ /* ### PROTECTED FUNCTIONS ####################################### # */ protected function calcValue( string $input, null|int $sourceBase = NULL, null|int $targetBase = NULL ) : int { $number = substr( $input, 0, 4 ); $serial = substr( $input, 4 );; if( $serial ) $checksum = $this->calcChecksumFromMac( $number, $serial ); else $checksum = $this->calcChecksumFromHex( $number ); // show( $number, $serial, $checksum, FALSE ); return $checksum; } protected function parseChecksum( int $acc, null|int $length = NULL, null|int $base = NULL ) : string { $code = $acc % pow( 16, $this->getChecksumLength() ); $code = dechex( $code ); $code = str_pad( (string)$code, $this->getChecksumLength(), '0', STR_PAD_LEFT ); $code = strtoupper( $code ); return $code; } protected function parseValidation( int $acc, null|int $length = NULL, null|int $base = NULL ) : string { $code = $acc * pow( $this->getValidationLength(), 2 ) % pow( 10, $this->getValidationLength() ); $code = str_pad( (string)$code, $this->getValidationLength(), '0', STR_PAD_LEFT ); $code = strtoupper( $code ); return $code; } /* ### PRIVATE FUNCTIONS ######################################## # */ // protected function calcNumber() { // if( $this->number ) return $this->number; // // $min = $this->number_min ? pow( 10, $this->number_length - 1 ) : 0; // // switch( $this->number_type ) { // case static::NumberTypeHex: do { $number = uuid("%0{$this->number_length}X"); } while( hexdec( $number ) < $min ); break; // case static::NumberTypeDec: $number = rand( $min, pow( 10, $this->number_length ) - 9 ); break; // } // // $number = str_pad( $number, $this->number_length, 0, STR_PAD_LEFT ); // $number = strtoupper( $number ); // // return $number; // } private function calcChecksumFromMac( string $number, null|string $serial ) { $checksum = explode( ":", $serial ); $checksum = array_map( "hexdec", $checksum ); $checksum = array_sum( $checksum ); $checksum = $checksum * $this->calcChecksumFromHex( $number ); return $checksum; } private function calcChecksumFromHex( string $number ) { $checksum = hexdec( $number ); $checksum = str_split( $checksum ); foreach( $checksum as $index => &$value ) { $value = $value * $index; } $checksum = array_sum( $checksum ); return $checksum; } /* ### MAGIC FUNCTIONS ########################################## # */ } /** Datei: plugins/cards/library/ARUNOX/Tools/Cardnumber/BaseCalc.php **/ * @copyright 2023 Sparky@ARUNOX * @version 1.0.0 * @since 1.0.0 File is created */ namespace ARUNOX\Tools\Cardnumber; use ARUNOX\Tools\Cardnumber; use InvalidArgumentException; class BaseCalc { /* ### CONSTANTS ################################################ # */ /* ### VARIABLES ################################################ # */ protected static int $NumberBase = 10; protected static int $NumberLength = 4; protected static int $DoubleCount = 2; protected static bool $DoubleRemove = FALSE; protected static int $ChecksumBase = 10; protected static int $ChecksumLevel = 0; protected static int $ChecksumLength = 2; protected static int $ValidationBase = 10; protected static int $ValidationLength = 3; protected int|null $number_base = NULL; protected int|null $number_length = NULL; protected int|null $double_count = NULL; protected bool|null $double_remove = NULL; protected int|null $checksum_level = NULL; protected int|null $checksum_length = NULL; protected int|null $validation_base = NULL; protected int|null $validation_length = NULL; /* ### PUBLIC STATIC FUNCTIONS ################################## # */ public static function setDefaultNumberBase( int $base ) { static::$NumberBase = $base; } public static function setDefaultNumberLength( int $length ) { static::$NumberLength = $length; } public static function setDefaultDoubleRemove( bool $remove ) { static::$DoubleRemove = $remove; } public static function setDefaultDoubleCount( int $count ) { static::$DoubleCount = $count; } public static function setDefaultChecksumBase( int $base ) { static::$ChecksumBase = $base; } public static function setDefaultChecksumLevel( int $level ) { static::$ChecksumLevel = $level; } public static function setDefaultChecksumLength( int $length ) { static::$ChecksumLength = $length; } public static function setDefaultValidationBase( int $base ) { static::$ValidationBase = $base; } public static function setDefaultValidationLength( int $length ) { static::$ValidationLength = $length; } /* ### PUBLIC FUNCTIONS ######################################### # */ public function getNumber() { return $this->__toString(); } public function getNumberBase( null|int $base = NULL ) { return $base ?? $this->number_base ?? static::$NumberBase; } public function getNumberLength( null|int $length = NULL ) { return $length ?? $this->number_length ?? static::$NumberLength; } public function setNumberBase( int $base ) { $this->number_base = $base; } public function setNumberLength( int $length ) { $this->number_length = $length; } public function calcNumber( null|int $length = NULL, null|int $base = NULL ) { $base = $this->getNumberBase( $base ); $length = $this->getNumberLength( $length ); $min = 0; $max = pow( $base, $length ) - 1; do { $number = random_int( $min, $max ); $number = $this->encode( $number, $base ); $number = str_pad( $number, $length, 0, STR_PAD_LEFT ); } while ( $this->hasDoubles( $number ) ); $number = strtoupper( $number ); return $number; } public function getDoubleRemove( null|bool $remove = NULL ) { return $remove ?? $this->double_remove ?? static::$DoubleRemove; } public function getDoubleCount( null|int $count = NULL ) { return $count ?? $this->double_count ?? static::$DoubleCount; } public function setDoubleRemove( bool $remove ) { $this->double_remove = $remove; } public function setDoubleCount( int $count ) { $this->double_count = $count; } public function calcDoubles( null|int $count = NULL, null|int $length = NULL, null|int $base = NULL ) { $count = $this->getDoubleCount( $count ); $base = $this->getNumberBase( $base ); $length = $this->getNumberLength( $length ); $total = gmp_pow( $base, $length ); $allowed = gmp_init(0 ); $k = array_fill(1, $count, 0 ); $factN = gmp_fact( $length ); $factR = []; for( $r = 0; $r <= $count; $r++ ) $factR[ $r ] = gmp_fact( $r ); $this->enumerateDoubles(1, $length, 0, $k, $count, $length, $base, $factN, $factR, $allowed ); $forbidden = gmp_sub( $total, $allowed ); $lossPct = gmp_strval( $forbidden ) * 100 / gmp_strval( $total ); return [ 'base' => $base, 'length' => $length, 'total' => gmp_strval( $total ), 'allowed' => gmp_strval( $allowed ), 'forbidden' => gmp_strval( $forbidden ), 'loss_pct' => number_format( $lossPct, 3 ), ]; } public function hasDoubles( string $number, null|int $count = NULL ) { if( !$this->getDoubleRemove() ) return FALSE; $input = str_split( $number ); $length = strlen( $number ); $count = $this->getDoubleCount( $count ); $counted = array_count_values( $input ); $filtered = array_filter( $counted, fn( $item ) => $item <= $count ); $newLength = array_sum( $filtered ); return $newLength != $length; } public function getChecksumBase( null|int $base = NULL ) { return $base ?? $this->checksum_base ?? static::$ChecksumBase; } public function getChecksumLevel( null|int $level = NULL ) { return $level ?? $this->checksum_level ?? static::$ChecksumLevel; } public function getChecksumLength( null|int $length = NULL ) { return $length ?? $this->checksum_length ?? static::$ChecksumLength; } public function setChecksumBase( int $base ) { $this->checksum_base = $base; } public function setChecksumLevel( int $level ) { $this->checksum_level = $level; } public function setChecksumLength( int $length ) { $this->checksum_length = $length; } public function calcChecksum( string $number, null|int $length = NULL, null|int $base = NULL, null|int $inputBase = NULL ) : string { $input = $this->getNumberBase( $inputBase ); $base = $this->getChecksumBase( $base ); $length = $this->getChecksumLength( $length ); $acc = $this->calcValue( $number, $input, $base ); $code = $this->parseChecksum( $acc, $length, $base ); // show( $input, $base, $length, $acc, $code, FALSE ); return $code; } public function getValidationBase( null|int $base = NULL ) { return $base ?? $this->validation_base ?? static::$ValidationBase; } public function getValidationLength( null|int $length = NULL ) { return $length ?? $this->validation_length ?? static::$ValidationLength; } public function setValidationBase( int $base ) { $this->validation_base = $base; } public function setValidationLength( int $length ) { $this->validation_length = $length; } public function calcValidation( string $number, null|int $length = NULL, null|int $base = NULL, null|int $inputBase = NULL ) : string { $input = $this->getNumberBase( $inputBase ); $base = $this->getValidationBase( $base ); $length = $this->getValidationLength( $length ); $acc = $this->calcValue( $number, $input, $base ); $code = $this->parseValidation( $acc, $length, $base ); // show($number, $base, $acc, $code, FALSE ); return $code; } public function check( string $input, null|string $serial = NULL ) : array { if( preg_match( '/(([a-z0-9]{4})([a-z0-9]{2}))([0-9]{3})?/i', $input, $matches ) ) { [ $input, $number, $uuid, $checksum, $cvc ] = array_pad( $matches, 5, NULL ); $numberLength = $this->getNumberLength() + $this->getChecksumLength(); $cvcLength = $this->getValidationLength(); if( strlen( $number ) < $numberLength ) return [ 100, "IDLENGTH" ]; if( $serial && $this->calcChecksum( $uuid . $serial ) == $checksum ) { return [ 200, "SERIAL" ]; } // show( $number, $numberLength, $uuid, $checksum, $this->calcChecksum( $uuid ), get_called_class(), FALSE ); if( $this->calcChecksum( $uuid ) == $checksum ) { return [ 200, "CHECKSUM" ]; } if( $cvc ) { if( strlen( $cvc ) < $cvcLength ) return [ 100, "CVCLENGTH" ]; if( $this->calcValidation( $number ) == $cvc ) { return [ 200, "CVC" ]; } } } return [ 500, "INVALID" ]; } /* ### PROTECTED FUNCTIONS ####################################### # */ protected function parseChecksum( int $acc, null|int $length = NULL, null|int $base = NULL ) : string { $base = $this->getChecksumBase( $base ); $level = $this->getChecksumLevel() - 1; $length = $this->getChecksumLength( $length ); $bucket = Cardnumber::getBucketSize(); $max = pow( $base, $length ); if( $bucket * ( $level + 1 ) > $max ) { throw new InvalidArgumentException('Bucket passt nicht in den verfügbaren Raum.'); } $acc = ( ($acc % $bucket) + $bucket ) % $bucket; $acc = $acc + $bucket * $level; $code = $this->encode( $acc, $base ); $code = str_pad( (string)$code, $length, '0', STR_PAD_LEFT ); $code = strtoupper( $code ); return $code; } protected function parseValidation( int $acc, null|int $length = NULL, null|int $base = NULL ) : string { $base = $this->getValidationBase( $base ); $length = $this->getValidationLength( $length ); $max = pow( $base, $length ); $acc = $acc % $max; $code = $this->encode( $acc, $base ); $code = str_pad( (string)$code, $length, '0', STR_PAD_LEFT ); $code = strtoupper( $code ); return $code; } protected function calcValue( string $input, null|int $sourceBase = NULL, null|int $targetBase = NULL ) : int { $input = preg_replace('/[^0-9A-F]/i', '', $input ); $input = strtoupper( $input ); $parts = str_split( $input ); $parts = array_map( fn( $part ) => $this->decode( $part, $sourceBase ), $parts ); $acc = 0; foreach( $parts as $index => $part ) $acc = ( $acc + $part * $index ); return $acc; } protected function encode( string $value, int $base ) : string { return base_convert( $value, 10, $base ); } protected function decode( string $value, int $base ) : string { // show( $value, $base, debug_backtrace(), FALSE ); return base_convert( $value, $base, 10 ); } /* ### PRIVATE FUNCTIONS ######################################## # */ private function enumerateDoubles( $r, $remaining, $usedSymbols, $k, $maxRepeat, $length, $base, $factN, $factR, &$allowed ) { if ($r === $maxRepeat) { // letzter Block: rest muss durch r teilbar sein if ($remaining % $r !== 0) return; $k[$r] = intdiv($remaining, $r); $symbolsNeeded = $usedSymbols + $k[$r]; if ($symbolsNeeded > $base) return; // Symbole wählen: Produkt der Binomiale (absteigend r) $waysPick = gmp_init(1); $left = $base; for ($rr = $maxRepeat; $rr >= 1; $rr--) { $kr = $k[$rr]; if ($kr > 0) { $waysPick = gmp_mul($waysPick, gmp_binomial($left, $kr)); $left -= $kr; } } // Anordnungen: n! / Π (r!)^{k_r} $den = gmp_init(1); for ($rr = 1; $rr <= $maxRepeat; $rr++) { $kr = $k[$rr]; if ($kr > 0) { // (r!)^kr $den = gmp_mul($den, gmp_pow($factR[$rr], $kr)); } } $perms = gmp_div_q($factN, $den); // Beitrag addieren $allowed = gmp_add($allowed, gmp_mul($waysPick, $perms)); return; } // Verteile k_r von 0..floor(remaining/r) $maxKr = intdiv($remaining, $r); for ($kr = 0; $kr <= $maxKr; $kr++) { $k[$r] = $kr; // früh abbrechen, wenn Symbolbedarf bereits > base wäre if ($usedSymbols + $kr > $base) break; $this->enumerateDoubles($r + 1, $remaining - $r * $kr, $usedSymbols + $kr, $k, $maxRepeat, $length, $base, $factN, $factR, $allowed); } } /* ### MAGIC FUNCTIONS ########################################## # */ public function __toString() { $number = $this->calcNumber(); return implode( "", [ $number, $this->calcChecksum( $number ), ]); } }