<?php
/**
 * @copyright (c) Kufer Software GmbH, Oderstraße 7, D-84453 Mühldorf
 */




class AbgleichAPI
{
  private $_symKey = '3615cbffe35f8d22';
  private $_iV = '1f842190adc05e68';
  private $_cryptutility;
  private $_publicKey;

  
  public $timeout = 90;

  
  
  public const ABGLEICH_NOTHING = -1;
  public const ABGLEICH_ANWESENHEITEN = 1;
  public const ABGLEICH_KURSKORREKTUR = 2;
  public const ABGLEICH_KURSPLANUNG = 3;
  public const ABGLEICH_KURSTAGKORREKTUR = 4; 
  public const ABGLEICH_NOTEN = 5;
  public const ABGLEICH_ABMELDUNG = 6;
  public const ABGLEICH_WTMELDUNG = 7;
  public const ABGLEICH_PUNKTE_BESTAETIGEN = 8; 
  public const ABGLEICH_ZERTIFIKAT_BEAUFTRAGEN = 9;
  public const ABGLEICH_TODO = 10;
  public const ABGLEICH_FEEDBACK = 11;
  public const ABGLEICH_PING = 12;
  public const ABGLEICH_EMAIL = 13;
  public const ABGLEICH_DOKUMENTE = 14;
  public const ABGLEICH_BELEGUNG_OLD = 15;
  public const ABGLEICH_KURSTAG = 16; 
  public const ABGLEICH_KURSTAGDELETE = 17; 
  public const ABGLEICH_DOKUMENTDELETE = 18; 
  public const ABGLEICH_DOKUMENTKORREKTUR = 19; 
  public const ABGLEICH_DOZKURSKORREKTUR = 20; 
  public const ABGLEICH_DOZKURS = 21; 
  public const ABGLEICH_DOZKURSDELETE = 22; 
  
  public const ABGLEICH_DOZENT = 23; 
  public const ABGLEICH_DOZENTENDELETE = 24; 
  public const ABGLEICH_DOZENTENKORREKTUR = 25; 
  public const ABGLEICH_PROTDRUCKLISTEN = 26;  

  public const ABGLEICH_WEB_PASSWORDS_USERS = 27; 
  public const ABGLEICH_WEB_USER = 28; 

  public const ABGLEICH_KOMMUNIKATION = 29; 
  public const ABGLEICH_KOMMKORREKTUR = 30; 
  public const ABGLEICH_KOMMDELETE = 31; 

  public const ABGLEICH_ADRESSE = 32; 
  public const ABGLEICH_ADRESSDELETE = 33; 
  public const ABGLEICH_ADRESSKORREKTUR = 34; 

  public const ABGLEICH_TEILNEHMER = 35; 
  public const ABGLEICH_TEILNEHMERDELETE = 36; 
  public const ABGLEICH_TEILNEHMERKORREKTUR = 37; 

  public const ABGLEICH_BELEGUNG = 38; 
  public const ABGLEICH_BELEGUNGDELETE = 39; 
  public const ABGLEICH_BELEGUNGKORREKTUR = 40; 

  public const ABGLEICH_GETANMELDESTATUS = 41; 
  

  public const ABGLEICH_ANMELDUNGRESERVIERUNGEMAILXML = 42; 

  
  
  
  public const ABGLEICH_KURS = 43;  
  public const ABGLEICH_KURSDELETE = 44; 
  public const ABGLEICH_KURSKORREKTUR_NEU = 45; 

  public const ABGLEICH_KURSZUSATZ = 46;  
  public const ABGLEICH_KURSZUSATZDELETE = 47; 
  public const ABGLEICH_KURSZUSATZKORREKTUR = 48;

  public const ABGLEICH_ANMELDUNGNEWSLETTER = 49; 
  public const ABGLEICH_INTERESSENBEKUNDUNG = 50;
  public const ABGLEICH_GETBELEGUNGSTATUS = 51; 
  public const ABGLEICH_UMMELDUNG = 52;         

  public const ABGLEICH_ANMELDUNGDIREKT = 53; 

  public const ABGLEICH_STAMMBVINSERT = 54; 
  public const ABGLEICH_STAMMBVUPDATE = 55; 
  public const ABGLEICH_STAMMBVDELETE = 56; 

  public const ABGLEICH_GETFREIEBERATUNGSTAGE = 57; 
  public const ABGLEICH_ZUSATZANMELDUNGBERATUNGSTAGE = 58; 

  public const ABGLEICH_GUTSCHEIN = 59; 

  
  
  public const ABGLEICH_GUTSCHEINKORREKTUR = 60; 

  public const ABGLEICH_KENNZEICHEN = 61; 
  public const ABGLEICH_KENNZEICHENDELETE = 62; 

  public const ABGLEICH_PORTRAIT_BEARBEITEN = 64; 

  public const ABGLEICH_ZAHLUNG = 65; 
  

  public const ABGLEICH_TEILNEHMER_MIT_DUBLETTENERKENNUNG = 67; 

  public const ABGLEICH_GETFEEDBACKCODE = 68; 
  public const ABGLEICH_FEEDBACKCODEKORREKTUR = 69; 

  public const ABGLEICH_TERMINKALENDER = 70; 
  

  public const ABGLEICH_ANMELDUNG_UPLOAD_SOFORT_EINBUCHEN = 74; 
  public const ABGLEICH_TEILNEHMER_IS_DUBLETTE = 75; 
  public const ABGLEICH_GET_TEILNEHMER = 76; 
  public const ABGLEICH_GETEINGETRAGENEBERATUNGSTAGE = 77; 

  public const ABGLEICH_GET_KOMMUNIKATION_TEILNEHMER = 78; 

  public const ABGLEICH_HAUPTUNTERKURSE = 79;      
                                           
  public const ABGLEICH_VERHALTEN = 80;        

  public const ABGLEICH_STAMMHOERER_HINZUFUEGEN = 81; 
  public const ABGLEICH_STAMMHOERER_IS_LOGIN_UNIQUE = 82; 

  public const ABGLEICH_MAP_MYSQLID = 83; 
  public const ABGLEICH_GET_BELEGUNG = 84; 
  public const ABGLEICH_STAMMHOERER_TN_DATEN_AKTUALISIEREN = 85;

  public const ABGLEICH_STAMMBV_GET = 86; 

  public const ABGLEICH_GET_HONORARINFOS_DOZENT = 87; 
  public const ABGLEICH_GETKURSTAG4KLASSENBUCH = 88; 
  public const ABGLEICH_GET_FACHINFOS_DOZENT = 89; 

  public const ABGLEICH_UPDATE_DEPUTAT = 90;

  public const ABGLEICH_GET_DOKUMENT = 91;
  public const ABGLEICH_GET_DOKUMENTWEB = 92; 

  public const ABGLEICH_GET_BELEGUNGEN_FOR_TEILNEHMER = 101;
  public const ABGLEICH_GETANMELDESTATUS_ARRAY = 102; 

  public const ABGLEICH_NEUES_DOZLOGIN = 103;

  public const ABGLEICH_GET_DOZ_KURS = 104;

  public const ABGLEICH_GET_KURSTAGE_RECHNUNGSSTATUS = 105;
  public const ABGLEICH_GET_VERTRAGSINFOS = 106;
  public const ABGLEICH_GET_KURS_RECHNUNGSSTATUS = 107;
  public const ABGLEICH_DATDOC_HINZUFUEGEN = 108; 

  public const ABGLEICH_GEBFORT = 109; 
  public const ABGLEICH_ABMELDUNG_KURS_MIT_ABO_GEBUEHR = 110;
  public const ABGLEICH_GETKONTOBLATTINFOS = 111;

  public const ABGLEICH_ZSA_BKBVTN_BEARBEITEN = 112;
  public const ABGLEICH_ZSA_BKBVTN_INSERT = 114;

  public const ABGLEICH_ZSA_TNZBZG_INSERT = 115;
  public const ABGLEICH_ZSA_TNZBZG_BEARBEITEN = 116;
  public const ABGLEICH_ZSA_TNZBZG_DELETE = 117;

  public const ABGLEICH_GET_TEILNEHMER_QUALIFIKATIONEN = 118;

  public const ABGLEICH_GET_DOZ_KURS_MIT_UNTERKURSE = 113;

  public const ABGLEICH_BELEGUNG_STATUS_AENDERN = 119;

  public const ABGLEICH_DOZBEWERBUNG = 120;

  public const ABGLEICH_BEZIEHUNG_DELETE = 121;
  public const ABGLEICH_BEZIEHUNG_INSERT = 122;
  public const ABGLEICH_DOKUMENTENLISTE = 123;

  

  public const ABGLEICH_UPDATE_NEWSL_INTERESSENSGEBIETE = 125;

  public const ABGLEICH_BENUTZER_BEARBEITEN = 126;

  public const ABGLEICH_STORNO_STAMMHOERER = 127;

  public const ABGLEICH_CHECK_SEPA_MANDAT = 128;

  public const ABGLEICH_WEB_PRIVILEGES_T_HINZUFUEGEN = 129;
  public const ABGLEICH_WEB_PRIVILEGES_T_BEARBEITEN = 130;

  public const ABGLEICH_EINZELVERANSTALTUNG_VERBUCHEN = 131;

  public const ABGLEICH_EPAYSTATUSAENDERN = 132; 


  public const ABGLEICH_FIRMA_STORNO = 134;

  public const ABGLEICH_GET_MYSQLID = 135;

  public const ABGLEICH_KURSAUSFALL_SETZEN = 136;

  public const ABGLEICH_NEUES_TNLOGIN = 137;

  public const  ABGLEICH_EPAY_NOTIFY = 138;

  public const  ABGLEICH_GET_KENNZAD = 139;
  public const ABGLEICH_KURSPLANUNG_DELETE = 140;
  public const ABGLEICH_KURSPLANUNG_UPDATE = 141;
  public const ABGLEICH_CHECK_KURSTAGE_RAUMPLANUNG = 142;
  public const ABGLEICH_GET_KURS_VERTRAGSSTATUS = 143;
  public const ABGLEICH_STORNO_TERMIN = 144;
  public const ABGLEICH_GET_EMAIL = 145;
  public const ABGLEICH_UNTERRICHTSMITTEL_HINZUFUEGEN = 147;
  public const ABGLEICH_CHECK_MAHNSTUFEN = 148;
  public const ABGLEICH_EMAIL_HINZUFUEGEN = 146;
  public const ABGLEICH_GET_KURS = 150;
  public const ABGLEICH_SPERRZEITINSERT = 151;
  public const ABGLEICH_GET_WEITERMELDECODE = 152;
  public const ABGLEICH_WEITERMELDECODE_EINGELOEST = 153;

  public const ABGLEICH_TEILNEHMER_IS_DUBLETTE_ELTERN_KIND = 149;

  public const ABGLEICH_INSERT_TNAUSBILDUNG = 154;
  public const ABGLEICH_GET_HONORAR_ABNAHME = 155;
  public const ABGLEICH_ZAHLUNG_UPDATE = 156;

  public const ABGLEICH_EPAY_CHECKMTIDRELEVANT = 158;
  public const ABGLEICH_STORNO_MIT_HASH = 159;

  public const ERSTELLE_ODER_AKTUALISIERE_HONORAR_ABNAHMEN_FUER_KURSTAGE = 160;

  public const ABGLEICH_GET_KINDER_FOR_TN_FROM_KURS = 161;

  public const ABGLEICH_ANMELDUNGDIREKT_JCSESCADA = 162; 

  public const ABGLEICH_DOZENT_IS_DUBLETTE = 163; 

  public const ABGLEICH_HONORAR_ABNAHME_AKZEPTIEREN = 164;
  public const ABGLEICH_HONORAR_ABNAHME_ABLEHNEN = 165;
  public const ABGLEICH_GET_HONORAR_ABNAHME_BY_ID = 166;

  public const ABGLEICH_RAUMRESERVIERUNG_INSERT = 167;
  public const ABGLEICH_RAUMRESERVIERUNG_UPDATE = 168;
  public const ABGLEICH_RAUMRESERVIERUNG_DELETE = 169;

  public const ABGLEICH_ARTIKELBESTELLUNG = 600;
  public const ABGLEICH_ARTIKELABFRAGE = 601;

  public const ABGLEICH_GUTSCHEIN_GUELTIGKEIT_PRUEFEN = 170;

  public const ABGLEICH_GET_KASSENZEICHEN_AND_ADD_TO_EINBUCHUNGSROUTINE = 180;

  public const ABGLEICH_GET_GUTSCHEIN = 171;

  
  public const ABGLEICH_INSERT_VORGESETZTER_ZU_BELEGUNG = 602;

  
  public const ABGLEICH_GET_VORGESETZTE_ZU_BELEGUNG = 603;

  
  public const ABGLEICH_GET_VORGESETZTE_ZU_VORGESETZTEN_AND_BELEGUNG = 604;

  
  public const ABGLEICH_INSERT_WUNSCHTERMIN = 605;

  
  public const ABGLEICH_UPDATE_WUNSCHTERMIN = 606;

  
  public const ABGLEICH_DELETE_WUNSCHTERMIN = 607;

  
  public const ABGLEICH_BELEGUNGEN_IN_KUFERSQL = 608;

  public const ABGLEICH_INSERT_TEILNEHMER_MAPPING_ANHAND_GUID = 900;
  public const ABGLEICH_INSERT_DOZENTEN_MAPPING_ANHAND_GUID = 901;

  
  public const ABGLEICH_GET_FINANZAMT_ZAHLUNGEN = 902;

  
  public const ABGLEICH_SAMMELVERTRAGE_FOR_DOZENT = 903;

  
  public const ABGLEICH_GET_SAMMELVERTRAG = 904;

  
  public const ABGLEICH_ACCEPT_OR_REJECT_SAMMELVERTRAG = 905;

  
  public const ABGLEICH_INSERT_TERMIN = 906;

  
  public const ABGLEICH_UPDATE_TERMIN = 907;

  
  public const ABGLEICH_DELETE_TERMIN = 908;

  
  public const ABGLEICH_PING_KUFERSQL_API = 909;

  
  public const ABGLEICH_GET_KUFERSQL_VERSION = 910;

  public function __construct()
  {
    $this->_cryptutility = CryptFactory::getCrypt();
  }

  
  private function _isPublicKeyValid($path)
  {
    $content = @\file_get_contents($path);
    if ($content === false) {
      return false;
    }

    $content = \trim($content);

    return (\StringOperations::strlen($content) >= 272)
      && (\StringOperations::substr($content, 0, 26) === '-----BEGIN PUBLIC KEY-----')
      && (\StringOperations::substr($content, -24) === '-----END PUBLIC KEY-----');
  }

  
  private function _loadPublicKey()
  {
    

    $paths = array(
      \FSOperations::path(\ConstantOperations::getPathForCoreSettings(), 'publickey.pem'),
      \FSOperations::path(BASEDIR, 'webtools', 'config', 'publickey.pem')
    );

    foreach ($paths as $path) {
      if ($this->_isPublicKeyValid($path)) {
        $key = file_get_contents($path);
        $this->_publicKey = openssl_get_publickey($key);
        if ($this->_publicKey !== false) {
          break;
        }
      }
    }

    if (empty($this->_publicKey)) {
      $opensslError = $this->openSSLErrorString();
      if ($opensslError !== '') {
        \Logger::logg('Fehler beim Lesen des Schlüssels: ' . $opensslError, 100);
      }
    }
  }

  private function _createSymKey()
  {
    
    $this->_symKey = $this->_createKey(16);
  }

  public function _createIV()
  {
    
    $this->_iV = $this->_createKey(16);
  }

  
  private function _createKey($length)
  {
    $randomBytes = \random\RandomBytes::getRandomBytes($length);
    if ($randomBytes !== false) {
      return $randomBytes;
    }

    throw new \RuntimeException('Es muss hier ein Zufallswert generiert worden sein!');
  }

  private function _getPublicKey()
  {
    return $this->_publicKey;
  }

  private function _getCryptutility()
  {
    return $this->_cryptutility;
  }

  private function _getSymKey()
  {
    return $this->_symKey;
  }

  private function _getIV()
  {
    return $this->_iV;
  }

  
  private function _verschluessleNachricht($pMessage)
  {
    
    

    $this->_loadPublicKey();
    $publicKey = $this->_getPublicKey();

    if (!is_null($publicKey)) {
      $this->_createSymKey();
      $this->_createIV();
    }

    
    $symKey = $this->_getSymKey();
    $iv = $this->_getIV();

    
    $encryptedMessage = $this->_getCryptutility()->aesEncrypt($pMessage, $symKey, $iv, Crypt::DATA_AS_HEX);

    $RSAencryptedSymKey = '';
    $RSAencryptedIV = '';
    if (!is_null($publicKey)) {
      
      if (openssl_public_encrypt($symKey, $RSAencryptedSymKey, $publicKey) === false) {
        Logger::logg('Fehler beim Verschlüsseln des symmetrischen Schlüssels: ' . $this->openSSLErrorString(), 100);
      }
      
      if (openssl_public_encrypt($iv, $RSAencryptedIV, $publicKey) === false) {
        Logger::logg('Fehler beim Verschlüsseln des IV: ' . $this->openSSLErrorString(), 100);
      }
    }

    $xmlEnvelope = new DOMDocument('1.0', 'UTF-8');
    $xmlEnvelope->formatOutput = true;
    $envelope = $xmlEnvelope->createElement('envelope');
    $key = $xmlEnvelope->createElement('key');
    $key->appendChild($xmlEnvelope->createTextNode(
      base64_encode($RSAencryptedSymKey)
    ));
    $envelope->appendChild($key);
    $xmlIv = $xmlEnvelope->createElement('iv');
    $xmlIv->appendChild($xmlEnvelope->createTextNode(base64_encode($RSAencryptedIV)));
    $envelope->appendChild($xmlIv);
    $msg = $xmlEnvelope->createElement('secretMessage');
    $msg->appendChild($xmlEnvelope->createTextNode($encryptedMessage));
    $envelope->appendChild($msg);
    $xmlEnvelope->appendChild($envelope);

    
    return $xmlEnvelope->saveXML();

    
  }

  private function openSSLErrors(): array
  {
    $errors = array();
    while (($err = openssl_error_string()) !== false) {
      $errors[] = $err;
    }

    return $errors;
  }

  private function openSSLErrorString()
  {
    $errors = $this->openSSLErrors();
    if (empty($errors)) {
      return '';
    }

    return implode(',', $errors) . ' ' . OPENSSL_VERSION_TEXT;
  }

  private function _loadSettings()
  {
    $allgsettings = AllgSettingsAPI::getAllgSettings();

    $result = new AbgleichApiSettings();



    $result->serverAddress = $allgsettings->dmzServer;
    $result->serverPort = $allgsettings->dmzPort;
    $result->aktiviert = $allgsettings->dmzEnabled;
    $result->dmzProtocolHttps = $allgsettings->dmzProtocolHttps;
    $result->dmzProtocolHttpsSelfSigned = $allgsettings->dmzProtocolHttpsSelfSigned;
    $result->apiKey = $allgsettings->dmzApiKey;

    return $result;
  }

  
  public function isAktiviert()
  {
    $settings = $this->_loadSettings();

    return (bool)$settings->aktiviert;
  }

  
  protected function createAuthenticationNode(AbgleichApiSettings $settings): XMLNode
  {
    $databaseId = \DBWorkWebReplikatSettings::getDefaultInstance()->getSettingValue('brDatabaseId');
    $authNode = new XMLNode('auth');
    $authNode->addChild(new XMLNode('webuserid', \SessionOperations::getUserID()));
    $authNode->addChild(new XMLNode('webpageid', \Kufer\Web\Classes\Api\WebpageUniqueIdApi::id()));
    if (!empty($databaseId->value)) {
      $authNode->addChild(new XMLNode('databaseid', $databaseId->value));
    }
    $authNode->addChild(new XMLNode('webversion', \Version::getCurrentVersion()->version));

    if (trim($settings->apiKey) !== '') {
      $authNode->addChild(new XMLNode('apikey', $settings->apiKey));
    }

    return $authNode;
  }

  
  protected function createInfoNode($operation): XMLNode
  {
    $infoNode = new XMLNode('info');
    $infoNode->addChild(new XMLNode('feature', $operation));
    $infoNode->addChild(new XMLNode('timestamp', DateTimeOperations::getTimeStampForAbgleich()));

    return $infoNode;
  }

  
  protected function createHighestPkSettings($operation): XMLNode
  {
    $dbwReplikatSettings = new DBWorkWebReplikatSettings();
    if ($operation == AbgleichAPI::ABGLEICH_PING && $dbwReplikatSettings->dboperation->getDBName() != DBTypeEnum::MYSQL) {
      $allHighestPkSettings = array($dbwReplikatSettings->getNewObject());
    } else {
      $allHighestPkSettings = $dbwReplikatSettings->getAllHighestPkSettings();
    }

    $highestPkSettingsNode = new XMLNode('highestpksettings');
    foreach ($allHighestPkSettings as $highestPkSetting) {
      $str = new StringObj($highestPkSetting->name);
      $splitStr = $str->split(':', 2);
      $tableName = $splitStr[0]->subString(9);

      $tempNode = new XMLNode('highestpksetting');
      $tempNode->addChild(new XMLNode('tablename', $tableName->toString()));
      $tempNode->addChild(new XMLNode('highestpk', $highestPkSetting->value));
      $highestPkSettingsNode->addChild($tempNode);
    }

    return $highestPkSettingsNode;
  }

  
  protected function createNutzlast($nutzlast, $pNoMapping): XMLNode
  {
    $nutzlastNode = $nutzlast->getXMLNode();
    if ($pNoMapping) {
      $nutzlastNode->addChild(new XMLNode('nomapping', 'W'));
    }

    return $nutzlastNode;
  }

  
  protected function normalizeSettings($settings)
  {
    if (\is_null($settings)) {
      $settings = $this->_loadSettings();
    }

    return $settings;
  }

  
  protected function sendMessageWithHttps($nachricht, $settings): AbgleichApiResult
  {
    $response = $this->sendBasDmzPostRequest($settings->serverAddress . ':' . $settings->serverPort, $nachricht, $settings->dmzProtocolHttpsSelfSigned);

    return $this->_parseResponse($response);
  }

  
  protected function sendMessageWithProprietary($nachricht, $settings): AbgleichApiResult
  {
    $host = $settings->serverAddress;
    $port = $settings->serverPort;

    $nachricht = $this->_verschluessleNachricht($nachricht);

    $errno = '';
    $errstr = '';
    $connection = @fsockopen($host, $port, $errno, $errstr, $this->timeout);
    

    if (!$connection) { 
      $errorMsg = "Kann Verbindung nicht herstellen: $errstr ($errno)";
      DebugOperations::sendDmzErrorMail($errorMsg);

      throw new AbgleichApiException(HTMLOperations::getAlertBox('<div class="alertbox_text">' . $errorMsg . '</div>', 'Fehler', 'in_background'), -1, -1);
    }

    
    if (fwrite($connection, \StringOperations::pad(StringOperations::strlen($nachricht), 10, '0', STR_PAD_LEFT) . $nachricht) === false) {
      $errorMsg = "Kann Verbindung nicht herstellen: $errstr ($errno)";
      DebugOperations::sendDmzErrorMail($errorMsg);

      throw new AbgleichApiException(HTMLOperations::getAlertBox('<div class="alertbox_text">' . $errorMsg . '</div>', 'Fehler', 'in_background'), -1, -1);
    }
    unset($nachricht);

    $encryptedResponse = '';
    $chunkLength = 1024 * 1024 * 10; 

    
    $throwConnectionError = static function() {
      $errorMsg = 'Kann Verbindung nicht herstellen: Timeout oder anderer Fehler beim Abholen der Daten';
      DebugOperations::sendDmzErrorMail($errorMsg);

      throw new AbgleichApiException(HTMLOperations::getAlertBox('<div class="alertbox_text">' . $errorMsg . '</div>', 'Fehler', 'in_background'), -1, -1);
    };

    $bytesToInt = static function(string $bytes) {
      $bytes = unpack('C*', $bytes);

      return $bytes[1] | $bytes[2] << 8 | $bytes[3] << 16 | $bytes[4] << 24;
    };

    $ivResponse = fread($connection, 16);
    if ($ivResponse === false || strlen($ivResponse) !== 16) {
      $throwConnectionError();
    }

    $encryptedMessageLengthBytes = fread($connection, 4);
    if ($encryptedMessageLengthBytes === false || strlen($encryptedMessageLengthBytes) !== 4) {
      $throwConnectionError();
    }
    $encryptedMessageLength = $bytesToInt($encryptedMessageLengthBytes);
    unset($encryptedMessageLengthBytes);

    $bytesConsumed = 0;
    while (!feof($connection) && $bytesConsumed < $encryptedMessageLength) {
      $getdata = fread($connection, $chunkLength);
      if ($getdata === false) {
        $throwConnectionError();
      }

      $bytesConsumed += strlen($getdata);
      $encryptedResponse .= $getdata;
      unset($getdata);
    }

    fclose($connection);
    unset($connection);

    if ($bytesConsumed !== $encryptedMessageLength) {
      $throwConnectionError();
    }

    unset($encryptedMessageLength, $bytesConsumed);

    $response = '';
    if (strlen($encryptedResponse) > 0) { 
      $response = $this->_getCryptutility()->aesDecrypt($encryptedResponse, $this->_getSymKey(), $ivResponse, Crypt::DATA_AS_IS);
      unset($encryptedResponse);
    }

    if ($response == '') {
      throw new AbgleichApiException(HTMLOperations::getAlertBox('<div class="alertbox_text">Server hat Verbindung unerwartet geschlossen.</div>', 'Fehler', 'in_background'), -1, -1);
    }

    return $this->_parseResponse($response);
  }

  
  protected function sendPreparedXMLNode($messageNode, $settings)
  {
    $settings = $this->normalizeSettings($settings);
    if (!$settings->aktiviert) {
      return new AbgleichApiResult(AbgleichApiResult::CODE_DISABLED, HTMLOperations::getInfoBox('<div class="infobox_text">Abgleich muss erst in den zentralen Einstellungen
                                                                                                    aktiviert werden.</div>', 'Hinweis', 'in_background'));
    }

    $nachricht = '<?xml version="1.0" encoding="UTF-8" ?>' . $messageNode->getXMLstring();
    if (DEVELOP) {
      Logger::logg('Nachricht um ' . DebugOperations::getCurrentTime() . ' an die DMZ: ' . $nachricht, 998);
    }

    $nachricht = StringOperations::utf8_encode($nachricht);

    if ($settings->dmzProtocolHttps == true) {
      $result = $this->sendMessageWithHttps($nachricht, $settings);
    } else {
      $result = $this->sendMessageWithProprietary($nachricht, $settings);
    }
    if (DEVELOP) {
      Logger::logg('response um ' . DebugOperations::getCurrentTime() . ': ' . DebugOperations::varDump($result), 201);
    }
    
    if ($result->code == AbgleichApiResult::CODE_SUCCESS || $result->code == AbgleichApiResult::CODE_DUPLICATE) {
      return $result;
    } else {
      throw new AbgleichApiException(HTMLOperations::getAlertBox('<div class="alertbox_text">Fehlerhaftes Ergebnis beim Abgleich: ' . $result->reason . '</div>', 'Fehler', 'in_background'), $result->code, $result->errorType);
    }
  }

  
  public function sendeNachricht($operation, $nutzlast, $settings = null, $pNoMapping = false)
  {
    $settings = $this->normalizeSettings($settings);

    
    $messageNode = new XMLNode('nachricht');
    $authNode = $this->createAuthenticationNode($settings);
    
    
    $messageNode->addChild($authNode);
    $infoNode = $this->createInfoNode($operation);
    $messageNode->addChild($infoNode);

    $highestPkSettingsNode = $this->createHighestPkSettings($operation);
    $messageNode->addChild($highestPkSettingsNode);

    if (!DEVELOP) {
      $identifier = self::getCopyrightIdentifier();
      $messageNode->addChild(new XMLNode('identity', $identifier));
    }

    
    $nutzlast = $this->createNutzlast($nutzlast, $pNoMapping);
    $messageNode->addChild($nutzlast);

    return $this->sendPreparedXMLNode($messageNode, $settings);
  }

  private function sendBasDmzPostRequest($url, $postData, $useSelfSigned)
  {
    $curl = RequestOperations::getStandardCurl($url, $postData, 'POST', $this->timeout);
    if ($useSelfSigned) {
      $paths = array(
        \FSOperations::path(\ConstantOperations::getPathForCoreSettings(), 'certificate.pem'),
        \FSOperations::path(BASEDIR, 'webtools', 'config', 'certificate.pem')
      );

      $certPath = null;
      foreach ($paths as $path) {
        if (file_exists($path)) {
          $certPath = $path;
          break;
        }
      }

      if (is_null($certPath)) {
        throw new AbgleichApiException(HTMLOperations::getAlertBox('<div class="alertbox_text">Es wurde keine \'certificate.pem\' Datei unter webtools/config gefunden. Die Übertragung via HTTP ist nur mit gültigem HTTPS Zertifikat erlaubt.</div>', 'Fehler', 'in_background'), -1, -1);
      }

      curl_setopt($curl, CURLOPT_CAINFO, $certPath);
      curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, true);
    }
    $result = curl_exec($curl);
    if ($result === false) {
      throw new AbgleichApiException(HTMLOperations::getAlertBox('<div class="alertbox_text">CURL Fehler bei DMZ Abfrage: ' . curl_error($curl) . '</div>', 'Fehler', 'in_background'), -1, -1);
    }
    curl_close($curl);

    return $result;
  }

  
  private function _parseResponse($response): AbgleichApiResult
  {
    if (empty($response)) {
      Logger::logg('leeres XML von der DMZ: ' . $response, 5502);

      return $this->getResultForInvalidXml();
    }

    $result = new AbgleichApiResult();

    $resultXml = new DOMDocument();

    
    @$resultXml->loadXML($response);
    $root = $resultXml->getElementsByTagName('result')->item(0);
    if (!($root instanceof \DOMElement)) {
      Logger::logg('falsches XML von der DMZ: ' . $response, 5503);

      return $this->getResultForInvalidXml($response);
    }

    $code = $root->getElementsByTagName('code')->item(0)->nodeValue;
    $reason = StringOperations::utf8_decode($root->getElementsByTagName('reason')->item(0)->nodeValue);
    $error_type = $root->getElementsByTagName('errortype')->item(0)->nodeValue;

    $result->code = $code;
    $result->reason = $reason;
    $result->errorType = $error_type;

    return $result;
  }

  private function getResultForInvalidXml($response = '')
  {
    $res = new AbgleichApiResult();
    $res->code = -1;
    $res->errorType = AbgleichApiResult::NO_CONNECTION;
    $res->reason = 'Es wurde kein valides XML zurückgegeben.';
    if ($response != '') {
      $res->reason .= ' Response: ' . $response;
    }

    return $res;
  }

  
  public function sendEmailViaDmz(
    string $pFrom,
    string $pSenderName,
    array $pTos,
    string $pSubject,
    string $pBodyPlain,
    string $pBodyHtml,
    array $pCcs = array(),
    array $pBccs = array(),
    array $pAttachments = array(),
    array $pHeaders = array('charset="UTF-8"'),
    string $pReplyTo = '',
    string $pReplyName = ''
  ): void {
    $fromNode = new XMLNode('from', $pFrom);
    $senderNameNode = new XMLNode('sendername', $pSenderName);

    $replyToNode = array();
    $replyNameNode = array();
    if (($pReplyTo ?? '') !== '') {
      $replyToNode[] = new XMLNode('replyto', $pReplyTo);
      $replyNameNode[] = new XMLNode('replyname', $pReplyName ?? '');
    }

    $replyTosNode = new XMLNode('replytos');
    $replyTosNode->addChilds($replyToNode);
    $replyNamesNode = new XMLNode('replynames');
    $replyNamesNode->addChilds($replyNameNode);

    $toNodes = array();
    foreach ($pTos as $to) {
      $toNodes[] = new XMLNode('to', $to);
    }

    $tosNode = new XMLNode('tos');
    $tosNode->addChilds($toNodes);

    $subjectNode = new XMLNode('subject');
    $subjectNode->setDataType('HTML');
    $subjectNode->setData(base64_encode($pSubject));

    $bodyHtmlNode = new XMLNode('bodyhtml');
    $bodyHtmlNode->setDataType('HTML'); 
    $bodyHtmlNode->setData(base64_encode($pBodyHtml));

    $bodyPlainNode = new XMLNode('bodyplain');
    $bodyPlainNode->setDataType('HTML'); 
    $bodyPlainNode->setData(base64_encode($pBodyPlain));

    $ccNodes = array();
    foreach ($pCcs as $cc) {
      $ccNodes[] = new XMLNode('cc', $cc);
    }

    $ccsNode = new XMLNode('ccs');
    $ccsNode->addChilds($ccNodes);

    $bccNodes = array();
    foreach ($pBccs as $bcc) {
      $bccNodes[] = new XMLNode('bcc', $bcc);
    }
    $bccsNode = new XMLNode('bccs');
    $bccsNode->addChilds($bccNodes);

    $attachmentNodes = array();
    foreach ($pAttachments as $attachment) {
      $node = new XMLNode('attachment');
      $contentNode = new XMLNode('content');

      if (is_a($attachment, 'EMailAttachment')) {
        $filenameNode = new XMLNode('filename', $attachment->filename);
        $contents = $attachment->content;
      } elseif (is_file($attachment)) {
        $filenameNode = new XMLNode('filename', StringOperations::cleanString(basename($attachment)));
        $contentNode->setDataType('HTML'); 
        $handle = fopen($attachment, 'rb');
        $contents = fread($handle, filesize($attachment));
        fclose($handle);
      } else {
        $filenameNode = new XMLNode('filename', 'Anhang.pdf');
        $contents = $attachment;
      }

      $contentNode->setData(base64_encode($contents));
      $node->addChild($filenameNode);
      $node->addChild($contentNode);
      $attachmentNodes[] = $node;
    }

    $attachmentsNode = new XMLNode('attachments');
    $attachmentsNode->addChilds($attachmentNodes);

    $encoding = 'UTF-8';
    $pattern = '/charset="([^"]+)"/i';
    $treffer = array();
    $headerNodes = array();
    foreach ($pHeaders as $header) {
      $node = new XMLNode('header');
      $node->setDataType('HTML'); 
      $node->setData($header);
      if (preg_match($pattern, $header, $treffer) === 1) {
        $encoding = $treffer[1];
      }
      $headerNodes[] = $node;
    }

    $headersNode = new XMLNode('headers');
    $headersNode->addChilds($headerNodes);

    $encodingNode = new XMLNode('encoding', $encoding);

    $email = new XMLNode('email');
    $email->addChild($fromNode);
    $email->addChild($senderNameNode);
    $email->addChild($replyTosNode);
    $email->addChild($replyNamesNode);
    $email->addChild($subjectNode);
    $email->addChild($bodyHtmlNode);
    $email->addChild($bodyPlainNode);
    $email->addChild($tosNode);
    $email->addChild($ccsNode);
    $email->addChild($bccsNode);
    $email->addChild($attachmentsNode);
    $email->addChild($headersNode);
    $email->addChild($encodingNode);

    $nutzlast = new Nutzlast('nutzlast');
    $nutzlast->addNode($email);

    $this->sendeNachricht(AbgleichAPI::ABGLEICH_EMAIL, $nutzlast);
  }

  
  public static function getXMLNode($pDbw, $pDatamodul, $fields, $pBezeichnung, $pAllowId = false)
  {
    $nothingToDo = true;
    $newNode = new XMLNode($pBezeichnung);
    
    foreach ($fields as $fieldName) {
      
      $isAndUpdateId = $pAllowId && $fieldName === 'id';
      
      
      if ($isAndUpdateId || $fieldName == 'mandatam' || $pDbw->canupdateDmz($fieldName)) {
        $fieldValue = $pDatamodul->getDMZValue($fieldName);
        $fieldAttr = '';
        if ($fieldValue === '') {
          $fieldValue = '-';
          $fieldAttr = 'leer="true"';
        }
        if (!$isAndUpdateId) {
          $fieldName = $pDbw->translateVarName($fieldName);
        }
        
        if ($fieldName === 'ort') {
          $matches = array();
          
          preg_match('/[0-9]+/', $fieldValue, $matches);
          if (count($matches) > 0) {
            $childNode = new XMLNode('plz', $matches[0], $fieldAttr);
            $newNode->addChild($childNode);
          }
        }

        $childNode = new XMLNode($fieldName, $fieldValue, $fieldAttr);
        $newNode->addChild($childNode);
        $nothingToDo = false;
      }
    }

    if ($nothingToDo) {
      return null;
    } else {
      return $newNode;
    }
  }

  
  public static function correctDatamodul(
    $pDbw,
    $pTuple,
    $pCorrectMode,
    $pXmlEinzelName,
    $pXmlMengenName,
    $pNameMaxPk,
    $pCorrectCodeInsert,
    $pCorrectCodeDelete,
    $pCorrectCodeSonstiges,
    $pNoMapping = false
  ) {
    $abgleichAPI = new AbgleichAPI();
    if (!$abgleichAPI->isAktiviert()) {
      return 'nichtAktiviert';
    }

    
    
    $datamodul = $pTuple->getFirst();
    $fields = $pTuple->getSecond();
    $datamodulOriginal = $pTuple->getThird();

    $datamodulNode = self::getXMLNode($pDbw, $datamodul, $fields, $pXmlEinzelName);

    if (!empty($datamodulNode)) {
      $datamodulNode->addChild(new XMLNode('id', $datamodul->id));
    }

    $nothingToDo = is_null($datamodulNode);  

    if (!$nothingToDo && $pCorrectMode == 'update') {
      $oldObjectNode = self::getXMLNode($pDbw, $datamodulOriginal, $fields, 'oldobject');
      $datamodulNode->addChild($oldObjectNode);
      $nothingToDo = $nothingToDo && ($oldObjectNode === null);
    }

    
    if (!$nothingToDo) {
      $message = new XMLNode($pXmlMengenName);
      
      
      $dbworkWebReplikatSettings = new DBWorkWebReplikatSettings();
      $webreplikatsetting = $dbworkWebReplikatSettings->getMaxPk($pNameMaxPk);
      $childNode = new XMLNode('highestpk', '-', 'leer="true"');
      if (isset($webreplikatsetting) && $webreplikatsetting->value != '') {
        $childNode = new XMLNode('highestpk', $webreplikatsetting->value);
      }
      $message->addChild($childNode);

      
      $message->addChild($datamodulNode);

      
      
      
      if ($pCorrectMode == 'insert') { 
        $operation = $pCorrectCodeInsert;
      } elseif ($pCorrectMode == 'delete') {
        $operation = $pCorrectCodeDelete;
      } else { 
        $operation = $pCorrectCodeSonstiges;
      }

      $abgleichAPI = new AbgleichAPI();
      
      $nutzlast = new Nutzlast($message);
      if ($pNoMapping) {
        $nutzlast->addNode(new XMLNode('nomapping', 'W'));
      }

      return $abgleichAPI->sendeNachricht($operation, $nutzlast);
    } 
    
    return 'noCorrection';
  }

  

  
  public function getOriginalId($pTableName, $pMySqlId)
  {
    if (!$this->isAktiviert()) {
      return 'nichtAktiviert';
    }

    $dbworkWebReplikatSettings = new DBWorkWebReplikatSettings();
    $webreplikatsetting = $dbworkWebReplikatSettings->getMaxPk($pTableName);
    if (isset($webreplikatsetting) && $webreplikatsetting->value != ''
            && $pMySqlId <= $webreplikatsetting->value) {
      return $pMySqlId;
    }

    $message = new XMLNode('map');
    $message->addChild(new XMLNode('tablename', $pTableName));
    $message->addChild(new XMLNode('id', $pMySqlId));

    $result = $this->sendeNachricht(AbgleichAPI::ABGLEICH_MAP_MYSQLID, new Nutzlast($message));

    Logger::logg($result, 999);
    
    if ($result->code == AbgleichApiResult::CODE_SUCCESS) {
      return $result->reason;
    } else {
      Logger::logg($result, 201);
    }

    return $result;
  }

  private static function getCopyrightIdentifier()
  {
    $dbw = new DBWorkBASysSettings();
    $settings = $dbw->getSpezielleSettings();

    return $settings->vhskurzbezeichnung . '_' . $settings->vhsbezeichnung;
  }

  public static function ping()
  {
    try {
      $abgleichapi = new AbgleichAPI();
      $abgleichapi->sendPingMessage();

      return true;
    } catch (Exception $e) {
      return false;
    }
  }

  public static function pingKuferSQLApi()
  {
    try {
      $abgleichapi = new AbgleichAPI();
      $abgleichapi->sendPingKuferSQLApiMessage();

      return true;
    } catch (Exception $e) {
      return false;
    }
  }

  
  public function sendPingMessage()
  {
    $nutzlast = new Nutzlast('Ping');
    $result = $this->sendeNachricht(AbgleichAPI::ABGLEICH_PING, $nutzlast);

    return $result->reason;
  }

  
  public function sendPingKuferSQLApiMessage(): \Kufer\Web\Classes\Datamodul\PingKuferSQLResult
  {
    $nutzlast = new Nutzlast('Ping');
    $result = $this->sendeNachricht(AbgleichAPI::ABGLEICH_PING_KUFERSQL_API, $nutzlast);

    $resultMsg = \JsonOperations::decodeJustString($result->reason);
    try {
      if (isset($resultMsg, $resultMsg->status, $resultMsg->version)) {
        $msg = new \Kufer\Web\Classes\Datamodul\PingKuferSQLResult();
        $msg->version = $resultMsg->version;
        $msg->status = $resultMsg->status;
        $msg->message = $resultMsg->message ?? null;

        return $msg;
      }
    } catch (\AbgleichApiException $ex) {
    }

    $msg = new \Kufer\Web\Classes\Datamodul\PingKuferSQLResult();
    $msg->version = 'unbekannt';
    $msg->status = \Kufer\Web\Classes\Datamodul\PingKuferSQLResult::STATUS_UNKNOWN;
    $msg->message = 'Antwort von BAS DMZ nicht erwartet';

    return $msg;
  }
} 


class AbgleichApiSettings
{
  
  public $serverAddress;
  
  public $serverPort;
  
  public $aktiviert;

  
  public $apiKey;

  public $dmzProtocolHttps;
  public $dmzProtocolHttpsSelfSigned;
}


class AbgleichApiResult
{
  
  public const CODE_SUCCESS = 0;
  
  public const CODE_ERROR = 1;
  
  public const CODE_DISABLED = 2;
  
  public const CODE_DUPLICATE = 3;


  
  public const KEIN_TYP = 0;

  
  public const FEHLER_NICHT_ANGEGEBEN = 1;

  
  public const KURSTAG_KORREKTUR_KEINE_EINGETRAGEN = 2;

  
  public const KURSTAG_KORREKTUR_TEILWEISE_EINGETRAGEN = 3;

  public const NOTEN_ID_NICHT_VORHANDEN = 4;

  public const NOTEN_INKONSISTENZ = 5;

  public const NOTEN_ENDE_UNGUELTIGES_FORMAT = 6;

  public const ABMELDUNG_ZAHLUNG_NICHT_OK = 7;

  public const BELEGUNG_NICHT_VORHANDEN = 8;

  public const TEILNEHMER_NICHT_VORHANDEN = 9;

  public const NO_CONNECTION = 10;

  public const KURS_CODE_NICHT_VORHANDEN = 11;

  public const KURS_CODE_BEREITS_EINGELOEST = 12;

  
  public const KURS_CODE_UNGUELTIGER_ANMELDESTATUS = 13;

  public const KURS_CODE_KURS_NICHT_EXTERN_UND_HAT_GEBUEHR = 14;

  public const KONFLIKT = 15;

  
  public const CONFLICT = 15;

  public const KENNZEICHEN_NICHT_GELOESCHT = 16;
  public const KENNZEICHEN_NICHT_HINZUGEFUEGT = 17;
  public const KENNZEICHEN_TYP_NICHT_UNTERSTUETZT = 18;
  public const FEEDBACK_CODE_NICHT_VORHANDEN = 19;

  
  public const MAPPING_ID_NICHT_VORHANDEN = 20;

  public const KEIN_PASSENDER_FILTER = 21;

  public const KURSTAG_NICHT_VORHANDEN = 22;

  public const MAPPING_ID_BEREITS_VORHANDEN = 23;

  public const BELEGUNG_STATUS_AENDERN_NICHT_ERLAUBT = 24;

  public const WEB_WBLOGINS_LOGIN_BEREITS_VORHANDEN = 25;

  public const ERGEBNIS_MENGE_ENTHAELT_FEHLER = 26;

  public const ARTIKEL_BESTELLUNG_PROBLEM = 27;

  public const EPAYMENT_DATEN_NICHT_VORHANDEN = 28;

  public const KURSAUSFALL_ZAHLUNGEN_VORHANDEN = 29;

  public const ZERTIFIKAT_FEHLER = 30;

  public const NEUES_LOGIN_PERSON_EXISTIERT_NICHT_ODER_GIBT_ES_MEHRFACH = 31;

  public const DOZENT_NICHT_VORHANDEN = 32;

  public const HONORAR_ABNAHME_GLEICHER_STATUS = 33;
  public const HONORAR_ABNAHME_FALSCHER_STATUS = 34;

  
  public $code;
  
  public $reason;

  public $errorType;

  public function __construct($code = self::CODE_SUCCESS, $reason = '', $et = self::KEIN_TYP)
  {
    $this->code = $code;
    $this->reason = $reason;
    $this->errorType = $et;
  }

  public function __toString()
  {
    return __CLASS__ . '( Code: ' . $this->code . '; errorType: ' . $this->errorType . '; reason: ' . $this->reason . ')';
  }
}

class AbgleichApiException extends Exception
{
  public $errorCode;
  public $errorType;
  public $errorReason;

  public function __construct($reason, $code = '', $type = '')
  {
    $this->errorReason = $reason;
    $this->errorCode = $code;
    $this->errorType = $type;
    $this->message = $reason;
  }
}

class DmzNotAvailableException extends AbgleichApiException
{
}