/** 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 ), ]); } } /** Datei: plugins/cards/library/ARUNOX/Plugins/Locations/Cards/Model.php **/ [ "message" => "Card wurde Standort hinzugefügt", "origin_hid" => $current->card->hashid, "target_hids" => [ $current->origin->hashid ], ], "card_id" => [ "message" => "Card wurde Standort hinzugefügt", "origin_hid" => $current->origin->hashid, "target_hids" => [ $current->card->hashid ], ], ]; } /* ### MAGIC FUNCTIONS ########################################## # */ } /** Datei: plugins/cards/library/ARUNOX/Plugins/Locations/Cards/API/v1/Routes.php **/ * @copyright 2025 ARUNOX * @version 1.1.0 * * @since 1.0.0 File is created */ namespace ARUNOX\Plugins\Locations\Cards\API\v1; use ARUNOX\Core\Hooks; use ARUNOX\Core\Router; use ARUNOX\Plugins\REST; class Routes { public static function register() : void { /* ### MAIN ROUTES ############################## # */ Router::addAll( Controller::class ); } } /** Datei: plugins/cards/library/ARUNOX/Plugins/Locations/Cards/API/v1/Controller.php **/ * @copyright 2025 ARUNOX * @version 1.1.0 * * @since 1.0.0 File is created */ namespace ARUNOX\Plugins\Locations\Cards\API\v1; use ARUNOX\HTTP; use ARUNOX\Plugins; use ARUNOX\API\PHP; use ARUNOX\Core\Hooks; use ARUNOX\API\BaseController; use ARUNOX\Database\BaseEntity; use ARUNOX\Plugins\REST\Generator\Base\BaseGenerator; #[PHP\Attributes\Controller\Label( "Karte", "Karten" )] #[PHP\Attributes\Controller\Icon( Plugins\REST\Generator\Config\Icon::CreditCard2Front )] #[PHP\Attributes\Controller\Route( "cards", Plugins\Locations\API\v1\Controller::class )] #[PHP\Attributes\Controller\Notifications( "rolechanged.0", '{$LABEL.singular} geändert zu Default' )] #[PHP\Attributes\Controller\Notifications( "rolechanged.1", '{$LABEL.singular} geändert zu User' )] #[PHP\Attributes\Controller\Notifications( "rolechanged.2", '{$LABEL.singular} geändert zu Admin' )] #[PHP\Attributes\Database\Definitions\Model( Plugins\Locations\Cards\Model::class )] class Controller extends BaseController { /* ### PUBLIC GET FUNCTIONS ##################################### # */ /** * @api * @method GET * * @route.path /locations/{origin}/cards * @route.description Listet alle Logeinträge */ public static function getAll( string $origin ) : array { $origin = Plugins\Locations\Cards\Model::resolveHashIDPrefix( $origin ); return Plugins\Locations\Cards\Model::getAll([ "origin_id" => $origin->id ] ); } /** * @api * @method GET * * @route.path /locations/cards/{hashid} * @route.description Gibt einen einzelnen Logeintrag anhand der ID zurück */ public static function getPrepared( string $hashid ) : BaseEntity | HTTP\Response { return parent::getPrepared( $hashid ); } /* ### PUBLIC SAVE FUNCTIONS #################################### # */ /** * @api * @method PUT * * @route.path /locations/cards/{hashid} * @route.description Speichert Änderungen an einem bestehenden Termin */ public static function savePrepared( string $hashid, array $data ) : HTTP\Response { return parent::savePrepared( $hashid, $data ); } /** * @api * @method POST * * @route.path /locations/cards * @route.description Erstellt einen neuen Kalender */ public static function createPrepared( array $data ) : HTTP\Response { return parent::createPrepared( $data ); } /** * @api * @method DELETE * * @route.path /locations/cards/{hashid} * @route.description Löscht ein Logeintrag anhand der ID */ public static function deletePrepared( string $hashid ) : HTTP\Response { return parent::deletePrepared( $hashid ); } /** * @api * @method PATCH * * @route.path /locations/cards/{hashid}/role * @route.description Wechselt den Status (aktiv/inaktiv) eines Kalenders */ public static function toggleRole( string $hashid ) : HTTP\Response { return static::togglePrepared( "role", $hashid ); } /* ### PUBLIC VALIDATE FUNCTIONS ################################ # */ /* ### PUBLIC REST RESPONSE FUNCTIONS ########################### # */ /** * @internal rest * @method PUT * * @route.path /locations/cards/{hashid} * @route.description REST-Antwort nach dem Speichern eines Termins */ public static function restResponseSavePrepared( string $hashid, array $data ) : HTTP\Response { return parent::restResponseSavePrepared( $hashid, $data ); } /** * @internal rest * @method POST * * @route.path /locations/cards * @route.description REST-Antwort nach dem Erstellen eines Kalenders */ public static function restResponseCreate( array $data ) : HTTP\Response { $result = static::createPrepared( $data ); if( $result->isStatus( 200 ) ) { Plugins\REST\Response::closeModals(); Plugins\REST\Response::refresh( "#cards" ); } return static::restResponse( $result ); } /** * @internal rest * @method DELETE * * @route.path /locations/cards/{hashid} * @route.description REST-Antwort nach dem Löschen eines Logeintrag */ public static function restResponseDelete( string $hashid ) : HTTP\Response { Plugins\REST\Response::closeModals(); $result = static::deletePrepared( $hashid ); if( $result->isStatus( 200 ) ) { Plugins\REST\Response::redirect( "/locations/cards" ); } return static::restResponse( $result ); } /** * @internal rest * @method PATCH * * @route.path /locations/cards/{hashid}/status * @route.description REST-Antwort zur Status-Umschaltung eines Kalenders */ public static function restResponseToggleStatusPrepared( string $hashid ) : HTTP\Response { $card = Plugins\Locations\Cards\Model::resolveHashIDPrefix( $hashid ); $result = Plugins\Cards\API\v1\Controller::toggleStatusPrepared( $card->card->hashid ); $card = Plugins\Locations\Cards\Model::resolveHashIDPrefix( $hashid ); if( $result->isStatus( 200 ) ) { Plugins\REST\Response::addUpdater( [ "data/table/row", $hashid ], $card ); } return static::restResponse( $result ); } /** * @internal rest * @method PATCH * * @route.path /locations/cards/{hashid}/role * @route.description REST-Antwort zur Status-Umschaltung eines Kalenders */ public static function restResponseToggleRole( string $hashid ) : BaseGenerator { return Plugins\REST\Response::addModal( "details", Plugins\Locations\Cards\Role::getSingular( "%t ändern" ) ) ->get() ->setApi( static::getRoute( $hashid ) ) ->setAction( static::getRoute( $hashid ), "PUT" ) ->addButton(...Hooks::filter( "rest.generator::button.type->submit", [], "Speichern", static::getRoute() )) ->addField(...Hooks::filter( "rest.generator::control.type->radio", [ "options" => Plugins\Locations\Cards\Role::getAll(), ], "role", TRUE, TRUE, "locations/cards" )) ; } /* ### PUBLIC REST CONFIG FUNCTIONS ############################# # */ /** * @internal rest * @method GET * * @route.path /locations/cards * @route.description REST-Konfiguration für Logbuch-Tabelle */ public static function restConfigOverview() : BaseGenerator { return Plugins\REST\Response::addWrapper( "details", static::getPlural() )->get() ->addTab( "Übersicht", FALSE )->get() ->setIcon( static::getIcon() ) ->addHeadline( static::getPlural(), 2 ) ->addCallable( [ static::class, "restConfigTable" ] ) ->addButton(...Hooks::filter( "rest.generator::button.name->create", [], "locations" )) ->parent() ; } /** * @internal rest * @method GET * * @route.path /locations/{origin}/cards/{$hashid} * @route.description REST-Konfiguration für Detailansicht eines Logeintrags */ public static function restConfigDetails( string $origin, string $hashid ) : BaseGenerator { return Plugins\REST\Response::addModal( "details", static::getSingular( "%t bearbeiten" ) ) ->setSize( "equal" ) ->setWidth("lg") ->get() ->setApi( static::getRoute( $hashid ) ) ->setAction( static::getRoute( $hashid ), "PUT" ) ->addTab( static::getSingular() )->get() ->addHeadline( static::getSingular(), 2 ) ->addCallable( [ static::class, "restConfigOrigin" ], $origin, $hashid ) ->addCallable( [ static::class, "restConfigInputs" ], $origin, $hashid ) ->parent() ->addCallable( [ Plugins\Logs\API\v1\Controller::class, "restConfigTab" ], $hashid ) ->addTab( "Speichern" )->get() ->setColor( Plugins\REST\Generator\Config\Color::Primary ) ->setIcon( "bi bi-save" ) ->setType( "submit" ); } /** * @internal rest * @method GET * * @route.path /locations/{origin}/cards/create * @route.description REST-Konfiguration für das Erstellen eines Kalenders (Formular) */ public static function restConfigCreateByOriginPrepared( bool | string $origin = FALSE ) : BaseGenerator { return parent::restConfigCreateByOriginPrepared( $origin ); } /** * @internal rest * @method GET * * @route.path /locations/cards/{hashid}/delete * @route.description REST-Konfiguration für die Lösch-Bestätigung eines Logeintrags */ public static function restConfigDeletePrepared( string $hashid ) : BaseGenerator { return parent::restConfigDeletePrepared( $hashid ); } /* ### PUBLIC REST EXTENSION FUNCTIONS ########################## # */ public static function restConfigTab( BaseGenerator $rest, null|string $hashid = NULL ) : void { $rest ->addTab( static::getPlural(), "cards" )->get() ->setIcon( static::getIcon() ) ->addCallable( [ static::class, "restConfigTable" ], $hashid ) ; } public static function restConfigTable( BaseGenerator $rest, null|string $hashid = NULL ) : BaseGenerator { return $rest ->addTable()->get() ->setApi( Plugins\Locations\API\v1\Controller::class::getRoute( $hashid, "cards" ) ) ->setAction( Plugins\Locations\API\v1\Controller::class::getRoute( $hashid, "cards", "{hashid}") ) ->setOrder( "uuid", "asc" ) ->setSearch( TRUE ) ->addButton(...Hooks::filter( "rest.generator::button.name->create", [], "locations/{$hashid}/cards" )) ->addColumn(...Hooks::filter( "rest.generator::column.type->status", [ "label" => Plugins\Cards\Status::getSingular(), "icon" => Plugins\Cards\Status::getIcon(), "options" => Plugins\Cards\Status::getAll(), ], "status", [], "locations/cards", "hashid" )) ->addColumn(...Hooks::filter( "rest.generator::column.type->icon", [ "label" => Plugins\Cards\Type::getSingular(), "icon" => Plugins\Cards\Type::getIcon(), "options" => Plugins\Cards\Type::getAll(), ], "card.type", [], "locations/cards", "hashid" )) ->addColumn(...Hooks::filter( "rest.generator::column.type->status", [ "label" => Plugins\Locations\Cards\Role::getSingular(), "icon" => Plugins\Locations\Cards\Role::getIcon(), "options" => Plugins\Locations\Cards\Role::getAll(), ], "role", [], "locations/cards", "hashid" )) ->addColumn(...Hooks::filter( "rest.generator::column.type->text", [ "label" => "UUID", ], "card.uuid", TRUE, TRUE, TRUE, "locations/cards" )) ; } public static function restConfigInputs( BaseGenerator $rest, bool|string $origin, bool|string $hashid = FALSE ) : BaseGenerator { return $rest->addField(...Hooks::filter( "rest.generator::control.type->select", [ "options" => Plugins\Locations\Cards\Role::getAll(), ], "role", TRUE, TRUE, "locations/cards" )); } public static function restConfigOrigin( BaseGenerator $rest, bool|string $origin, bool|string $hashid = FALSE ) : BaseGenerator { $origin = Plugins\Locations\Cards\Model::resolveHashIDPrefix( $origin ); $current = Plugins\Locations\Cards\Model::resolveHashIDPrefix( $hashid ); $excluded = Plugins\Locations\Cards\Model::getAllValue([ "origin_id" => $origin->id ], "card_id" ); $rest->addField(...Hooks::filter( "rest.generator::control.type->hidden", [], "origin_id", $origin->id, "locations/cards" )); if( $hashid === FALSE ) { return $rest->addField(...Hooks::filter( "rest.generator::control.type->select", [ "options" => Plugins\Cards\Model::getAsOptions( "uuid", excluded: $excluded ) , ], "card_id", TRUE, TRUE, "locations/cards" )); } return $rest->addField(...Hooks::filter( "rest.generator::control.type->hidden", [], "card_id", $current->id, "locations/cards" )); } } /** Datei: plugins/cards/library/ARUNOX/Plugins/Locations/Cards/Role.php **/ isset( $item["toggle"] ) && $item["toggle"] ); $options = array_map( fn( $item ) => $item["name"], $options ); ksort( $options ); $this->value++; if( $this->value > array_key_last( $options ) ) { $this->value = array_key_first( $options ); } return $this->value; } } /** Datei: plugins/cards/library/ARUNOX/Plugins/Locations/Cards/Entity.php **/ origin_id ); } public function getCard() : bool|BaseEntity { return Cards\Model::get( $this->card_id ); } /* ### MAGIC FUNCTIONS ########################################## # */ } /** Datei: plugins/cards/library/ARUNOX/Plugins/Cards/Model.php **/ Status::Lost ], $labelColumn, $valueColumn, $excluded ); } public static function verify( string $input, string|NULL $serial = NULL ) { return Cardnumber::check( $input, $serial ); } /* ### MAGIC FUNCTIONS ########################################## # */ } /** Datei: plugins/cards/library/ARUNOX/Plugins/Cards/API/v1/Routes.php **/ * @copyright 2025 ARUNOX * @version 1.1.0 * * @since 1.0.0 File is created */ namespace ARUNOX\Plugins\Cards\API\v1; use ARUNOX\Core\Hooks; use ARUNOX\Core\Router; use ARUNOX\Tools\Cardnumber; class Routes { public static function register() : void { /* ### MAIN ROUTES ############################## # */ Router::addAll( Controller::class ); /* ### MENU ROUTES ############################## # */ Hooks::add('router::get->api.v1.rest.routes', function( $routes ) { $routes[] = [ "path" => Controller::getRoute(), "label" => Controller::getPlural(), "icon" => Controller::getIcon(), "menu" => "management", ]; return $routes; }, 50 ); } public static function init() : void { Cardnumber::setLevelClass( 1, Cardnumber\v1::class ); Cardnumber::setLevelClass( 2, Cardnumber\v2::class ); Cardnumber::setDefaultLevel( 2 ); } public static function redirect( $route ) : array { [ $uri, $method ] = $route; if( $uri === Controller::getRoute( "0:rest", "verify" ) ) { Hooks::remove( "LOGIN_REDIRECT" ); } return $route; } } /** Datei: plugins/cards/library/ARUNOX/Plugins/Cards/API/v1/Controller.php **/ * @copyright 2025 ARUNOX * @version 1.1.0 * * @since 1.0.0 File is created */ namespace ARUNOX\Plugins\Cards\API\v1; use ARUNOX\HTTP; use ARUNOX\Plugins; use ARUNOX\API\PHP; use ARUNOX\Core\Hooks; use ARUNOX\API\BaseController; use ARUNOX\Database\BaseEntity; use ARUNOX\Plugins\REST\Generator\Base\BaseGenerator; #[PHP\Attributes\Controller\Label( "Karte", "Karten" )] #[PHP\Attributes\Controller\Icon( Plugins\REST\Generator\Config\Icon::CreditCard2Front )] #[PHP\Attributes\Controller\Route( "cards" )] #[PHP\Attributes\Controller\Notifications( "statuschanged.0", '{$LABEL.singular} wurde erfolgreich erstellt' )] #[PHP\Attributes\Controller\Notifications( "statuschanged.1", '{$LABEL.singular} wurde erfolgreich beschrieben' )] #[PHP\Attributes\Controller\Notifications( "statuschanged.2", '{$LABEL.singular} wurde erfolgreich aktiviert' )] #[PHP\Attributes\Controller\Notifications( "statuschanged.3", '{$LABEL.singular} wurde erfolgreich deaktiviert' )] #[PHP\Attributes\Controller\Notifications( "NOTFOUND", 'Card not Found' )] #[PHP\Attributes\Controller\Notifications( "FORBIDDEN", 'Card not allowed' )] #[PHP\Attributes\Controller\Notifications( "CHECKSUM", 'LOGIN: by CHECKSUM' )] #[PHP\Attributes\Controller\Notifications( "SERIAL", 'LOGIN: by SERIAL' )] #[PHP\Attributes\Controller\Notifications( "CVC", 'LOGIN: by CVC' )] #[PHP\Attributes\Controller\Notifications( "INVALID", 'LOGIN: FAILED' )] #[PHP\Attributes\Database\Definitions\Model( Plugins\Cards\Model::class )] class Controller extends BaseController { /* ### PUBLIC GET FUNCTIONS ##################################### # */ /** * @api * @method GET * * @route.path /cards * @route.description Listet alle Logeinträge */ public static function getAllPrepared() : array { return parent::getAllPrepared(); } /** * @api * @method GET * * @route.path /cards/{hashid} * @route.description Gibt einen einzelnen Logeintrag anhand der ID zurück */ public static function getPrepared( string $hashid ) : BaseEntity | HTTP\Response { return parent::getPrepared( $hashid ); } /** * @api * @method POST * * @route.path /cards/verify * @route.description Gibt einen einzelnen Logeintrag anhand der ID zurück */ public static function verify( $data ) : HTTP\Response { [ $code, $error, $number ] = Plugins\Cards\Model::verify( $data["cardid"], $data["serial"] ); if( $code == 200 ) { if( $data["action"] == "login" ) { if( $card = Plugins\Cards\Model::getByUUID( $number ) ) { if( $card->status->isActive() && $card->type->isLogin() ) { Plugins\Users\Model::setCurrent( $card->user ); } else [ $code, $error ] = [ "403", "FORBIDDEN" ]; } else [ $code, $error ] = [ "404", "NOTFOUND" ]; } } return Plugins\REST\Response::autoResponse( $code, static::getNotify( $error ), NULL, "NFC Scan" ); } /* ### PUBLIC SAVE FUNCTIONS #################################### # */ /** * @api * @method PUT * * @route.path /cards/{hashid} * @route.description Speichert Änderungen an einem bestehenden Termin */ public static function savePrepared( string $hashid, array $data ) : HTTP\Response { return parent::savePrepared( $hashid, $data ); } /** * @api * @method POST * * @route.path /cards * @route.description Erstellt einen neuen Kalender */ public static function createPrepared( array $data ) : HTTP\Response { return parent::createPrepared( $data ); } /** * @api * @method DELETE * * @route.path /cards/{hashid} * @route.description Löscht ein Logeintrag anhand der ID */ public static function deletePrepared( string $hashid ) : HTTP\Response { return parent::deletePrepared( $hashid ); } /** * @api * @method PATCH * * @route.path /cards/{hashid}/status * @route.description Wechselt den Status (aktiv/inaktiv) eines Kalenders */ public static function toggleStatusPrepared( string $hashid ) : HTTP\Response { return parent::toggleStatusPrepared( $hashid ); } /* ### PUBLIC VALIDATE FUNCTIONS ################################ # */ /* ### PUBLIC REST RESPONSE FUNCTIONS ########################### # */ /** * @internal rest * @method PUT * * @route.path /cards/{hashid} * @route.description REST-Antwort nach dem Speichern eines Termins */ public static function restResponseSavePrepared( string $hashid, array $data ) : HTTP\Response { return parent::restResponseSavePrepared( $hashid, $data ); } /** * @internal rest * @method POST * * @route.path /cards * @route.description REST-Antwort nach dem Erstellen eines Kalenders */ public static function restResponseCreate( array $data ) : HTTP\Response { $result = static::createPrepared( $data ); if( $result->isStatus( 200 ) ) { Plugins\REST\Response::closeModals(); Plugins\REST\Response::redirect( "/cards/{$result->getBody("data", "hashid")}" ); } return static::restResponse( $result ); } /** * @internal rest * @method DELETE * * @route.path /cards/{hashid} * @route.description REST-Antwort nach dem Löschen eines Logeintrag */ public static function restResponseDelete( string $hashid ) : HTTP\Response { Plugins\REST\Response::closeModals(); $result = static::deletePrepared( $hashid ); if( $result->isStatus( 200 ) ) { Plugins\REST\Response::redirect( "/cards" ); } return static::restResponse( $result ); } /** * @internal rest * @method PATCH * * @route.path /cards/{hashid}/status * @route.description REST-Antwort zur Status-Umschaltung eines Kalenders */ public static function restResponseToggleStatusPrepared( string $hashid ) : HTTP\Response { return parent::restResponseToggleStatusPrepared( $hashid ); } /** * @internal rest * @method POST * * @route.path /cards/verify * @route.description REST-Konfiguration für Benutzer Lougoin */ public static function restResponseVerify( array $data ) : HTTP\Response { return static::restResponse( static::verify( $data ), function(){ $referer = HTTP\Request::referer(); redirect( "/api/v1/rest/{$referer["path"]}" ); }); } /* ### PUBLIC REST CONFIG FUNCTIONS ############################# # */ /** * @internal rest * @method GET * * @route.path /cards * @route.description REST-Konfiguration für Logbuch-Tabelle */ public static function restConfigOverview() : BaseGenerator { return Plugins\REST\Response::addWrapper( "details", static::getPlural() )->get() ->addTab( "Übersicht", FALSE )->get() ->setIcon( static::getIcon() ) ->addHeadline( static::getPlural(), 2 ) ->addCallable( [ static::class, "restConfigTable" ] ) ->addButton(...Hooks::filter( "rest.generator::button.name->create", [], "cards" )) ; } /** * @internal rest * @method GET * * @route.path /cards/{hashid} * @route.description REST-Konfiguration für Detailansicht eines Logeintrags */ public static function restConfigDetails( string $hashid ) : BaseGenerator { return Plugins\REST\Response::addWrapper( "details", static::getSingular( "%t bearbeiten" ) ) ->setBacklink( static::getRoute() ) ->get() ->setApi( static::getRoute( $hashid ) ) ->setAction( static::getRoute( $hashid ), "PUT" ) ->addTab( static::getSingular() )->get() ->addHeadline( static::getSingular(), 2 ) ->addCallable( [ static::class, "restConfigInputs" ], $hashid ) ->parent() ->addCallable( [ Plugins\Logs\API\v1\Controller::class, "restConfigTab" ], $hashid ) ->addTab( "Speichern" )->get() ->setColor( Plugins\REST\Generator\Config\Color::Primary ) ->setIcon( "bi bi-save" ) ->setType( "submit" ); } /** * @internal rest * @method GET * * @route.path /cards/create * @route.description REST-Konfiguration für das Erstellen eines Kalenders (Formular) */ public static function restConfigCreatePrepared() : BaseGenerator { return parent::restConfigCreatePrepared(); } /** * @internal rest * @method GET * * @route.path /cards/{hashid}/delete * @route.description REST-Konfiguration für die Lösch-Bestätigung eines Logeintrags */ public static function restConfigDeletePrepared( string $hashid ) : BaseGenerator { return parent::restConfigDeletePrepared( $hashid ); } /* ### PUBLIC REST EXTENSION FUNCTIONS ########################## # */ public static function restConfigTable( BaseGenerator $rest ) : BaseGenerator { return $rest ->addTable()->get() ->setApi( static::getRoute() ) ->setAction( static::getRoute("{hashid}") ) ->setOrder( "uuid", "asc" ) ->setSearch( TRUE ) ->addCallable( [ static::class, "restConfigColumns" ] ) ; } public static function restConfigColumns( BaseGenerator $rest ) : BaseGenerator { return $rest ->addColumn(...Hooks::filter( "rest.generator::column.type->status", [ "label" => Plugins\Cards\Status::getSingular(), "icon" => Plugins\Cards\Status::getIcon(), "options" => Plugins\Cards\Status::getAll(), ], "status", [], "cards", "hashid" )) ->addColumn(...Hooks::filter( "rest.generator::column.type->icon", [ "label" => Plugins\Cards\Type::getSingular(), "icon" => Plugins\Cards\Type::getIcon(), "options" => Plugins\Cards\Type::getAll(), ], "type", [], "cards", "hashid" )) ->addColumn(...Hooks::filter( "rest.generator::column.type->text", [], "uuid", TRUE, TRUE, TRUE, "cards" )) ->addColumn(...Hooks::filter( "rest.generator::column.type->text", [ "label" => "Standort", ], "location.name", TRUE, TRUE, TRUE, "cards" )) ->addColumn(...Hooks::filter( "rest.generator::column.type->text", [ "label" => "Benutzer", ], "user.username", TRUE, TRUE, TRUE, "cards" )) ->addColumn(...Hooks::filter( "rest.generator::column.type->datetime", [], "date_edit", TRUE, FALSE, TRUE, "cards" )) ; } public static function restConfigInputs( BaseGenerator $rest ) : BaseGenerator { return $rest ->addField(...Hooks::filter( "rest.generator::control.type->text", [], "name", TRUE, "cards" )) // ->addField(...Hooks::filter( "rest.generator::control.type->text", [], "additional", FALSE, "cards" )) // ->addField(...Hooks::filter( "rest.generator::control.type->text", [], "address", FALSE, "cards" )) // ->addField(...Hooks::filter( "rest.generator::control.type->text", [], "zipcode", FALSE, "cards" )) // ->addField(...Hooks::filter( "rest.generator::control.type->text", [], "city", FALSE, "cards" )) ; } } /** Datei: plugins/cards/library/ARUNOX/Plugins/Cards/Type.php **/ isset( $item["toggle"] ) && $item["toggle"] ); $options = array_map( fn( $item ) => $item["name"], $options ); ksort( $options ); $this->value++; if( $this->value > array_key_last( $options ) ) { $this->value = array_key_first( $options ); } return $this->value; } public function isLogin() : bool { return in_array( $this->value, [ static::User, static::Administrator ] ); } } /** Datei: plugins/cards/library/ARUNOX/Plugins/Cards/Entity.php **/ user_id ) { $location = Plugins\Locations\Users\Model::get([ "user_id" => $this->user_id, ]); } else { $location = Plugins\Locations\Cards\Model::get([ "card_id" => $this->id, ]); } return $location ? $location->origin : NULL; } public function getLocations() : array { if( $this->user_id ) { return Plugins\Locations\Users\Model::getAll([ "user_id" => $this->user_id, ]); } return Plugins\Locations\Cards\Model::getAll([ "card_id" => $this->id, ]); } public function getUser() : null|BaseEntity { return $this->user_id ? Plugins\Users\Model::get( $this->user_id ) : NULL; } /* ### MAGIC FUNCTIONS ########################################## # */ } /** Datei: plugins/cards/library/ARUNOX/Plugins/Cards/Status.php **/ isset( $item["toggle"] ) && $item["toggle"] ); $options = array_map( fn( $item ) => $item["name"], $options ); ksort( $options ); $this->value++; if( $this->value > array_key_last( $options ) ) { $this->value = array_key_first( $options ); } return $this->value; } } /** Datei: plugins/cards/hooks/pwa.php **/ * @copyright 2025 ARUNOX * @version 1.0.0 * @since 1.0.0 File is created */ use ARUNOX\Core\Hooks; /** Datei: plugins/cards/hooks/init.php **/ * @copyright 2025 ARUNOX * @version 1.0.0 * @since 1.0.0 File is created */ use ARUNOX\Core\Hooks; use ARUNOX\Plugins\Cards; use ARUNOX\Plugins\Locations; Hooks::add('public::api->init', [ Cards\API\v1\Routes::class, "register" ] ); Hooks::add('public::api->init', [ Locations\Cards\API\v1\Routes::class, "register" ] ); Hooks::add('config::plugins->loaded', [ Cards\API\v1\Routes::class, "init" ] ); Hooks::add('router::resolve->before', [ Cards\API\v1\Routes::class, "redirect" ] ); Hooks::add('theme::scripts->before', function() { ?>config.OLD', function( config ) { config.content ??= {}; config.content.message ??= []; if( ARUNOX.Tools?.NFC.exists() ) { config.content.message.push('scan access card'); } return config; }); ARUNOX.Hooks.register( 'template.login->afterRender.OLD', function() { ARUNOX.Hooks.once( 'wrapper.dynamic->afterRender.OLD', function() { const section = this.instance; const parent = this.instance.parent; if( ARUNOX.Tools?.NFC.exists() ) { const NFCConfig = Object.assignRecursive( {}, section.element.ARX.storage.config, { color : 'danger', header : { after : false, before : 'Permission', }, aside : { side : 'left', }, footer : { after : { content : [ ARUNOX.Theme.htmlTag.config( 'button', { id : 'NFC_grant_authorization', icon : 'bi-check-lg', label : 'Berechtigung erteilen', onClick : () => ARUNOX.Tools.NFC.read(), style : { width : '100%', maxWidth : '100%', }, }), ], }, }, }); NFCConfig.content = [ ARUNOX.Theme.htmlTag.config( 'div', { classes : 'error__wrapper', content : [ ARUNOX.Theme.htmlTag.config( 'div', { classes : 'error', content : [ ARUNOX.Theme.htmlTag.config( 'div', { classes : 'error__icon', innerHTML : ``, }), ARUNOX.Theme.htmlTag.config( 'div', { classes : 'error__text', content : [ ARUNOX.Theme.htmlTag.config( 'h1', { classes : 'error__caption', textNode : 'NFC Gerät', }), ARUNOX.Theme.htmlTag.config( 'div', { classes : 'error__message', textNode : 'Fehlende Berechtigung', }), ], }), ], }), ], }) ]; const NFCQuery = parent.render( 'layout/section', NFCConfig ); const NFCHide = () => { NFCQuery.element.remove(); parent.append( section ); }; const NFCShow = ( denied ) => { section.element.remove(); parent.append( NFCQuery ) if( denied ) NFCQuery.elements.footer.elements.after.remove(); }; ARUNOX.Tools.NFC.onDenied = () => NFCShow( true ); if( typeof ARUNOX.Tools.NFCReadLogin === 'function' ) { ARUNOX.Tools.NFC.onRead = ARUNOX.Tools.NFCReadLogin; } ARUNOX.Tools.NFC.onGranted = NFCHide; ARUNOX.Tools.NFC.onPrompt = () => NFCShow( false ); ARUNOX.Tools.NFC.init(); } if( ARUNOX.Tools?.Keyboard ) { const loginForm = section.element.querySelector('form'); const Keyboard = parent.render( 'layout/section', { name : 'keyboard', color : 'primary', style : { order : 2, flexBasis : '200%', }, header : { after : '#', }, aside : { side : 'right', }, footer : { before : 'Keyboard', } }); const KBCopen = section.element.ARX.elements.header.addRight( true ); const KBCclose = Keyboard.element.ARX.elements.footer.addRight( true ); const KBCnumber = Keyboard.element.ARX.elements.header.addAfter( true ); const KBCback = Keyboard.element.ARX.elements.header.addBefore( true ); const KBCclear = Keyboard.element.ARX.elements.header.addRight( true ); const KBBopen = KBCopen.replace( 'button', { htmlTag : 'a', icon : 'bi bi-keyboard', title : 'Keyboard', color : { color: 'default', target: 'button' }, }); const KBBclose = KBCclose.replace( 'button', { htmlTag : 'a', icon : 'bi bi-caret-down-fill', title : 'Schließen', }); const KBBclear = KBCclear.replace( 'button', { htmlTag : 'a', color : 'danger', icon : 'bi bi-trash', title : 'Zurücksetzen', }); const KBBback = KBCback.replace( 'button', { htmlTag : 'a', color : 'secondary', icon : 'bi bi-backspace', title : 'Löschen', }); const KBEnumber = KBCnumber.element.querySelector('.frame__title'); KBCopen.reset(); ARUNOX.Tools.Keyboard.init({ visible : false, type : 'base36', closeBtn : KBBclose, openBtn : KBBopen, backBtn : KBBback, clearBtn : KBBclear, number : KBEnumber.ARX, content : Keyboard.elements.content, onHide : () => { loginForm.style.display = ''; KBCopen.replace( KBBopen ); Keyboard.element.remove(); }, onShow : () => { loginForm.style.display = 'none'; KBCopen.reset(); parent.append( Keyboard ) }, onBackHide : () => { KBCback.reset(); }, onBackShow : () => { KBCback.replace( KBBback ); }, onClearHide : () => { KBCclear.reset(); }, onClearShow : () => { KBCclear.replace( KBBclear ); }, onRefresh : ( value ) => ARUNOX.Tools.NFCManuelLogin( value ), }); } }); }); /** Datei: plugins/cards/assets/js/arunox/hooks/templates/_general.js **/ /** * cards/arunox/hooks/templates/v.js * * Some Javascript codes without further description * * @author Sparky * @copyright 2025 ARUNOX * @version 1.0.0 * @since 1.0.0 File is created */ ARUNOX.Hooks.register( 'template->beforeRender.OLD', function() { if( ARUNOX.Tools?.NFC.exists() ) { ARUNOX.Tools.NFC.onRead = null; ARUNOX.Tools.NFC.onError = null; ARUNOX.Tools.NFC.onError = null; ARUNOX.Tools.NFC.onPrompt = null; ARUNOX.Tools.NFC.onDenied = null; ARUNOX.Tools.NFC.onGranted = null; ARUNOX.Tools.NFC.onWritten = null; ARUNOX.Tools.NFC.init(); } }, 0 ); ARUNOX.Hooks.register( 'wrapper->beforeRender.OLD', function() { if( this.instance.storage.config.title == 'PWAs' ) { // console.log( 'wrapper->beforeRender', this.instance.element, this.instance.storage.config ); } }, 0 ); /** Datei: plugins/cards/assets/js/arunox/nfc.js **/ window.ARUNOX ??= {}; ARUNOX.Tools ??= {}; ARUNOX.Tools.NFC = { ndef : null, abort : null, onRead : null, onAbort : null, onError : null, onPrompt : null, onDenied : null, onGranted : null, onWritten : null, log : function() { // console.log( 'NFC Reader', ...arguments ); }, stop : function() { this.log( 'Stop' ); this.abort.abort(); }, exists : function() { return typeof NDEFReader !== 'undefined'; }, write : async function( records, onSuccess, onAbort ) { this.ndef = new NDEFReader(); this.abort = new AbortController(); this.abort.signal.onabort = event => { this.log( 'OnAbortWrite', event ); this._call( onAbort || this.onAbort, event ); }; try { await this.ndef.write({ records: records }, { signal: this.abort.signal }) .then( () => { let data = []; for( let record of records ) { data.push({ serial : null, type : record.recordType || null, media : record.mediaType || null, data : record.data || null, lang : record.lang || null, encoding: record.encoding || null, }); } this.log( 'Start Write', data ); return this._call( onSuccess || this.onWritten );; }); } catch( error ) { this.log( 'onErrorWrite', error ); this._call( onError || this.onError, error ); } }, read : async function( onSuccess, onAbort, onError ) { this.ndef = new NDEFReader(); this.abort = new AbortController(); this.abort.signal.onabort = event => { this.log( 'OnAbortRead', event ); this._call( onAbort || this.onAbort, event ); }; this.log( 'Read' ); try { const decoder = new TextDecoder(); await this.ndef.scan({ signal: this.abort.signal }); this.log( 'Waiting' ); this.ndef.addEventListener( 'readingerror', event => { this.log( 'onReadingError', 'Argh! Cannot read data from the NFC tag. Try another one?', event ); this._call( onError || this.onError, event ); }); this.ndef.addEventListener( 'reading', ({ message, serialNumber }) => { this.log( 'Reading' ); let records = []; for( let [ index, record ] of Object.entries( message.records ) ) { if( record.data ) { let data = decoder.decode( record.data ); records.push({ serial : serialNumber, type : record.recordType, media : record.mediaType, data : data, lang : record.lang, encoding: record.encoding, }); } else { records.push({ serial : serialNumber, type : record.recordType, media : record.mediaType, lang : record.lang, encoding: record.encoding, }); } } return this._call( onSuccess || this.onRead, records, serialNumber );; }); } catch( error ) { this.log( 'onErrorRead', error ); this._call( onError || this.onError, error ); } }, init : function( onGranted, onPrompt, onDenied ) { navigator.permissions.query({ name: 'nfc' }).then( permissionStatus => { if( permissionStatus.state === 'prompt' ) this._call( onPrompt || this.onPrompt ); else if( permissionStatus.state === 'denied' ) this._call( onDenied || this.onDenied ); else if( permissionStatus.state === 'granted' ) { this._call( onGranted || this.onGranted ); if( this._init ) return; this._init = true; this.read(); } permissionStatus.onchange = _ => { if( permissionStatus.state == 'granted' ) this._call( onGranted || this.onGranted ); else if( permissionStatus.state === 'denied' ) this._call( onDenied || this.onDenied ); }; }); }, _init : false, _call : function( callback, ...args ) { if( typeof callback === 'function' ) { return callback.apply( this, args ); } return false; }, }; /** Datei: plugins/cards/assets/js/cards.js **/ /** * cards/cards.js * * Some Javascript codes without further description * * @author Sparky * @copyright 2025 ARUNOX * @version 1.0.0 * @since 1.0.0 File is created */ window.ARUNOX = window.ARUNOX || {}; ARUNOX.Tools ??= {}; ARUNOX.Tools.NFCReadLogin = function( records, serial ) { for( const record of records ) { const matches = record.data?.match(/ARXCID=(.*)/ ); if( matches ) { ARUNOX.Router.handle({ route : `/cards/verify?redirect=/rest${location.pathname}`, method : 'POST', }, { action : 'login', cardid : matches[1], serial : serial, }, {}, 'rest' ); return; } } ARUNOX.Theme.wrapper.render('notifications', { content : [ { type : 'generator', generator : 'components/notification', config : { type : 'warning', icon : 'bi bi-exclamation-triangle-fill', duration : null, title : 'Not recognized', message : 'Unknown card scanned', content : [] } } ] }); } ARUNOX.Tools.NFCManuelLogin = async function( value ) { if( value?.length == 9 ) { const result = await ARUNOX.Router.handle({ route : `/cards/verify?redirect=/rest${location.pathname}`, method : 'POST', }, { action : 'login', cardid : value, serial : null, }, {}, 'rest' ); ARUNOX.Tools.Keyboard.clear(); return; } if( value?.length > 9 ) { ARUNOX.Theme.wrapper.render('notifications', { content : [ { type : 'generator', generator : 'components/notification', config : { type : 'warning', icon : 'bi bi-exclamation-triangle-fill', duration : null, title : 'Card Number', message : 'Maximum length reached', content : [] } } ] }); ARUNOX.Tools.Keyboard.clear(); return; } } /** Datei: plugins/cards/config.php **/ * @copyright 2025 ARUNOX * @version 0.1.0 * * @since 0.1.0 initial version */