<?php

/*
 * Copyright CADMAI Software GmbH.
 * All rights reserved.
 *
 * The redistributor / user does not have the right to change, translate,
 * back-develop, decompile or disassemble the software.
 *
 * Redistributions must retain the above copyright notice,
 * this list of conditions and the following disclaimer.
 *
 * Neither the name of CADMAI Software GmbH nor the names of its
 * contributors may be used to endorse or promote products derived from
 * this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception;

require_once('Poi3d/3rdParty/tcpdf/tcpdf.php');
require_once('Poi3d/3rdParty/PHPMailer/src/Exception.php');
require_once('Poi3d/3rdParty/PHPMailer/src/PHPMailer.php');
require_once('Poi3d/3rdParty/PHPMailer/src/SMTP.php');
require_once'Poi3d/3rdParty/PNServer/autoloader.php';

use SKien\PNServer\PNPayload;
use SKien\PNServer\PNServer;
use SKien\PNServer\PNSubscription;
use SKien\PNServer\PNVapid;

$GLOBALS['Poi3dVer'] = '3.4';

function iCheckFilenameForRead ($fileName)
{
  if(strpos($fileName, 'workGroups.json') !== false) return true;
  if(strpos($fileName, 'poi3dLicenses.json') !== false) return true;
  if(strpos($fileName, 'annotations.json') !== false) return true;
  if(strpos($fileName, 'locations.json') !== false) return true;
  if(strpos($fileName, 'instructions.json') !== false) return true;
  if(strpos($fileName, 'materialSets.json') !== false) return true;
  if(strpos($fileName, 'fileAttachments.json') !== false) return true;
  if(strpos($fileName, 'boms.json') !== false) return true;
  if(strpos($fileName, 'docChat.json') !== false) return true;
  if(strpos($fileName, 'grpChat.json') !== false) return true;
  if(strpos($fileName, 'usrChat.json') !== false) return true;
  if(strpos($fileName, 'fileAccess.json') !== false) return true;
  if(strpos($fileName, 'sensors.json') !== false) return true;
  if(strpos($fileName, 'docAttributes.json') !== false) return true;

  return false;
}
function iCheckFilenameForWrite ($fileName)
{
  if(strpos($fileName, 'documents.json') !== false) return true;
  if(strpos($fileName, 'annotations.json') !== false) return true;
  if(strpos($fileName, 'locations.json') !== false) return true;
  if(strpos($fileName, 'instructions.json') !== false) return true;
  if(strpos($fileName, 'materialSets.json') !== false) return true;
  if(strpos($fileName, 'fileAttachments.json') !== false) return true;
  if(strpos($fileName, 'boms.json') !== false) return true;
  if(strpos($fileName, 'docChat.json') !== false) return true;
  if(strpos($fileName, 'usrChat.json') !== false) return true;
  if(strpos($fileName, 'grpChat.json') !== false) return true;
  if(strpos($fileName, 'fileAccess.json') !== false) return true;
  if(strpos($fileName, 'sensors.json') !== false) return true;
  if(strpos($fileName, 'docAttributes.json') !== false) return true;

  return false;
}

function iLoadCryptoKeys ($commKey, $pushKey)
{
  $filename = 'portalConfig.json';

  if(file_exists($filename)) {
    $f = iReadFile ( $filename );
    $fContent = json_decode ( $f );

    $commKey->publickey = $fContent->pbEncCommKey;
    $commKey->secret = $fContent->prEncCommKey;

    $pushKey->subject = 'https://poi3d.com/';
    $pushKey->publicKey = $fContent->pbPushKey;
    $pushKey->privateKey = $fContent->prPushKey;

    return true;
  }
  return false;
}
function iLoadMailConfiguration ($mailConfig)
{
  $mailConfig->host = "";
  $mailConfig->user = "";
  $mailConfig->password = "";
  $mailConfig->port = "";
  $mailConfig->from = "";
  $mailConfig->from_name = "";
  $mailConfig->bcc = "";
  $mailConfig->overwrite_recipient = "";	// overwrite recipient mail for testing purposes
  $mailConfig->from_2fa_pwd = "";	// used for 2fa mails and password reset
  $mailConfig->from_2fa_pwd_name = "";
  $mailConfig->footer_en = 'Please do not reply to this mail. Use the email section of the communication dialog instead';
  $mailConfig->footer_de = 'Bitte antworten Sie nicht auf diese E-Mail. Verwenden Sie stattdessen den E-Mail Bereich im Kommunikations-Dialog';

  $filename = 'portalConfig.json';

  if(file_exists($filename)) {
    $f = iReadFile ( $filename );
    $fContent = json_decode ( $f );

    if(!empty( $fContent->mlHost)) $mailConfig->host = $fContent->mlHost;
    if(!empty( $fContent->mlUser)) $mailConfig->user = $fContent->mlUser;
    if(!empty( $fContent->mlPwd)) $mailConfig->password = $fContent->mlPwd;
    if(!empty( $fContent->mlPort)) $mailConfig->port = $fContent->mlPort;
    if(!empty( $fContent->mlFrom)) $mailConfig->from = $fContent->mlFrom;
    if(!empty( $fContent->mlFromTtl)) $mailConfig->from_name = $fContent->mlFromTtl;
    if(!empty( $fContent->mlBcc)) $mailConfig->bcc = $fContent->mlBcc;
    if(!empty( $fContent->mlFrom2fa)) $mailConfig->from_2fa_pwd = $fContent->mlFrom2fa;
    if(!empty( $fContent->mlFrom2faTtl)) $mailConfig->from_2fa_pwd_name = $fContent->mlFrom2faTtl;
  }
}
function iCheckCaller()
{
  $callers = debug_backtrace();
  for ( $i = 1; $i < count( $callers ); $i++ ) {
    if(
        (!empty($callers[$i]['file']) &&
        ((strpos(strtolower($callers[$i]['file']), 'controller.php') !== false)||
        (strpos(strtolower($callers[$i]['file']), 'poi3daiconnector.php') !== false))
        ))
      return true;
  }
  return false;
}
function iGetCurrentContentDirectory ()
{
  return './Poi3dUserContent/';
}
function iGetCurrentHomeDirectory ()
{
  return './Poi3dUserData/';
}
function iIsPoi3dEnv ()
{
  if (strpos($_SERVER['HTTP_HOST'], 'mypoi3d.com') !== false)
    return false;

  if (strpos($_SERVER['HTTP_HOST'], 'poi3d-portal.com') !== false)
    return true;

  if (strpos($_SERVER['HTTP_HOST'], 'poi3d.com') !== false)
    return true;
  else
    return false;
}

function iDecrypt ( $hexdata, &$ret, &$errorText ) {
  $crypto = (object) [];
  $vapid = (object) [];
  $errorText = "";

  if(iLoadCryptoKeys($crypto, $vapid) == false) {
    $errorText = 'unknown configuration';
    return 1;
  }

  $keypair = sodium_crypto_box_keypair_from_secretkey_and_publickey( sodium_hex2bin($crypto->secret), sodium_hex2bin($crypto->publickey ));

  $data = sodium_hex2bin( $hexdata );

  $decrypted = sodium_crypto_box_seal_open( $data, $keypair );

  if ( $decrypted === false ) {
    $errorText = 'Not decrypted.';
    return 1;
  }

  $ret = json_decode( $decrypted );

  return 0;
}

function applySaltToChar($code, $salt) {
  return array_reduce(array_map('ord', str_split($salt)), function($a, $b) {
    return $a ^ $b;
  }, $code);
}

function iDecodeText($textIn, $salt)
{
  $hexPairs = str_split($textIn, 2);

  $decodedChars = array_map(function($hex) use ($salt) {
    $charCode = hexdec($hex);
    return applySaltToChar($charCode, $salt);
  }, $hexPairs);

  return implode('', array_map('chr', $decodedChars));
}

function iDecodeAttachment ( $fileName, $salt ) {
  if (file_exists($fileName)) {
    $f = iReadFile ( $fileName );
    $content = json_decode ( $f );
    if(isset($content->enc1)) {
      $realTxt = iDecodeText($content->enc1,$salt);
      if (strpos($realTxt, 'enc:') !== false) {
        $realTxt = substr($realTxt,4);
        iWriteFile($fileName, $realTxt);
      }
      else
        unlink($fileName);
    }
  }
}

function iLoadUserBaseData ( $data, &$ret, &$errorText )
{
  $errorText = "";
  //std user: "{"iD":{"uNm":"TestReader","pWd":"5674657","uId":"gkjgjhg"}}"
  //admin override: "{"iD":{"uNm":"TestReader","pWd":"5674657","aNm":"Poi3dAdmin","uId":"gkjgjhg"}}"

  if ( empty ( $data ) || empty ( $data->iD ) || (empty ( $data->iD->pWd ) && empty( $data->iD->vDt ) ) || empty ( $data->iD->uNm ) ) {
    $errorText = "invalid user token";
    return 1;
  }

  $username = $data->iD->uNm;

  //echo($username.'\n');
  // prevent special characters
  if ( !preg_match("`^[@.\w-]+$`", $username ) ) {
    $errorText = "invalid username or password";
    return 2;
  }

  $file = 'Poi3dUsers/' . $username . '.json';
  //echo($file.'\n');

  if ( !file_exists ( $file ) ) {
    $errorText = "invalid username or password";
    return 3;
  }

  $f = iReadFile ( 'Poi3dUsers/' . $username . '.json' );
  $user = json_decode ( $f );

  if ( empty( $user ) ) {
    $errorText = "invalid json file";
    return 4;
  }

  //
  // overwrite password if received asymmetrical encrypted
  if ( !empty( $data->iD->vDt ) ) {
    if ( iDecrypt( $data->iD->vDt, $vDt, $errorText ) ) {
      $errorText = 'decryption error: ' . $errorText;
      return 8;
    }

    //GetTimestamp (null, $time, $et);
    $date = new DateTime();
    $unix_seconds = $date->setTimezone(new \DateTimeZone('UTC'))->format('U');
    $unix_seconds = intval( $unix_seconds / 30 );

    if ( abs( $unix_seconds - $vDt->time ) >= 2 ) {
      $t_diff = ($unix_seconds - $vDt->time) * 30;
      $errorText = 'time does not match by ' . $t_diff . ' seconds. Server: ' . $unix_seconds . ', client: ' . $vDt->time;
      return 9;
    }

    $data->iD->pWd = $vDt->pwd;
  }

  if (!empty ( $data->iD->aNm ) ){
    $af = iReadFile ( 'Poi3dUsers/' . $data->iD->aNm . '.json' );
    $aUser = json_decode ( $af );

    if ( !password_verify ( $data->iD->pWd, $aUser->pWd ) ) {
      $errorText = "invalid username or password";
      return 5;
    }
  }
  else {
    $tmpPwdSuccess = false;

    if ( !empty( $user->tmpPwdTimestamp ) && (time() - intval( $user->tmpPwdTimestamp ) < 60*60*1) ) {
      if ( password_verify( $data->iD->pWd, $user->tmpPwd) ) {
        $tmpPwdSuccess = true;
      }
    }

    if ( !$tmpPwdSuccess && !password_verify ( $data->iD->pWd, $user->pWd ) ) {
      $errorText = "invalid username or password";
      return 6;
    }
  }

  if ( empty ( $data->iD->aNm ) && (!empty ( $user->lck ) && ( $user->lck == true ))) {
    $errorText = "user is locked";
    return 7;
  }

    $ret = $user;

  return 0;
}

function iGetUser ( $username, &$ret, &$errorText )
{
  $ret = [];
  $errorText = "";

  if ( empty ( $username )) {
    $errorText = "invalid iD data structure";
    return 1;
  }

  // prevent special characters
  if ( !preg_match("`^[@.\w-]+$`", $username ) ) {
    $errorText = "invalid username or password";
    return 1;
  }

  $file = 'Poi3dUsers/' . $username . '.json';

  if ( !file_exists ( $file ) ) {
    $errorText = "invalid username or password";
    return 1;
  }

  $f = iReadFile ( 'Poi3dUsers/' . $username . '.json' );
  $user = json_decode ( $f );

  if ( empty( $user ) ) {
    $errorText = "invalid json file";
    return 1;
  }

  $ret = $user;

  return 0;
}

function iGetUserLanguage( $userId, &$lang, &$errorText )
{
  $lang = "en";
  $errorText = "";

  if ( empty ( $userId )) {
    $errorText = "invalid iD data structure";
    return 1;
  }

  $userProfileFname  = 'Poi3dUserData/' . $userId . '/profile.json';

  $prof = (object) [];
  if(file_exists($userProfileFname)) {
    $p = iReadFile ( $userProfileFname );
    $prof = json_decode ( $p );
    $lang = $prof->uiLng;
  }
  else {
    $errorText = "couldn't open " . $userProfileFname;
    return 2;
  }

  return 0;
}

function iGetUserLanguageAndEmail ($userAlias, $userId, &$lang, &$eml, &$errorText )
{
  $lang = "en";
  $eml = "";
  $errorText = "";

  if ( empty ( $userAlias )) {
    $errorText = "invalid iD data structure (alias)";
    return 1;
  }

  if ( empty ( $userId )) {
    $errorText = "invalid iD data structure (id)";
    return 2;
  }

  $userFname = 'Poi3dUsers/' . $userAlias . '.json';

  $prof = (object) [];
  if(file_exists($userFname)) {
    $p = iReadFile ( $userFname );
    $prof = json_decode ( $p );
    $eml = $prof->emL;
  }
  else {
    $errorText = "couldn't open " . $userFname;
    return 3;
  }

  $userProfileFname  = 'Poi3dUserData/' . $userId . '/profile.json';

  $prof = (object) [];
  if(file_exists($userProfileFname)) {
    $p = iReadFile ( $userProfileFname );
    $prof = json_decode ( $p );
    $lang = $prof->uiLng;
  }
  else {
    $errorText = "couldn't open " . $userProfileFname;
    return 4;
  }

  return 0;
}

function iResizeImage ( $filename, $newHeight, &$error )
{
  list( $w, $h ) = getimagesize ( $filename );

  if ( $h <= $newHeight ) {
    return 0;
  }

  $newWidth = $newHeight / $h * $w;

  $isPNG = strtolower( substr ( $filename, -3 ) ) == 'png';

  $newImage = imagecreatetruecolor( $newWidth, $newHeight );

  if ( $isPNG )
    $orgImage = imagecreatefrompng ( $filename );
  else
    $orgImage = imagecreatefromjpeg ( $filename );

  if ( !$orgImage ) {
    $error = "couldn't parse image";
    return 1;
  }

  if ( !imagecopyresampled ( $newImage, $orgImage, 0, 0, 0, 0, $newWidth, $newHeight, $w, $h ) ) {
    $error = "couldn't resmaple image";
    return 1;
  }

  if ( $isPNG )
    $e = imagepng ( $newImage, $filename );
  else
    $e = imagejpeg ( $newImage, $filename, 95 );

  if ( !$e ) {
    $error = "couldn't create resampled image";
    return 1;
  }

  return 0;
}

function iIsDir ( $path )
{
  // this function is not 100% correct

  if ( strlen ( $path ) < 4 )
    return true;

  if ( substr ( $path, -4, 1 ) == '.' || substr ( $path, -5, 1 ) == '.' ) {
    return false;
  }

  return true;
}

function iCheckGlobalAccess ($filename)
{
  $checkName = strtolower($filename);

  if(
    (strpos($checkName, 'poi3dusercontent') !== false)&&
    (strpos($checkName, 'chatfiles') !== false)) {
      return true;
  }

  if(
    (strpos($checkName, 'poi3dusercontent') !== false)&&
    (strpos($checkName, 'profile.json') !== false)) {
    $content = iGetJsonFromFile($filename,false);
    //echo ("fContent " . json_encode ($content)). "\n";
    if(isset($content->data->pub) && ($content->data->pub == true))
      return true;
    else
      return false;
  }

  if (
    (strpos($checkName, 'poi3dusercontent') !== false)||
    (strpos($checkName, 'poi3duserdata') !== false)||
    (strpos($checkName, 'poi3dusers') !== false)||
    (strpos($checkName, 'portalconfig') !== false)
    ) {
    return false;
  }
  return true;
}

function iCheckLock ( $user, $fileName, &$errorText, &$lockUser)
{
  $lckFlNm = $fileName . '_lock';
  $errorText = "";
  $lockUser = "";

  if ( !file_exists( $lckFlNm )) {
    return 0;
  }

  $f = iReadFile ( $lckFlNm);
  $content = json_decode ( $f );

  if ($content->lckUserId != $user->uId) {
    $errorText = "locked by user " . $content->lckUserNm;
    $lockUser = $content->lckUserNm;
    return 1;
  }

  return 0;
}

function iGetAccessIds ( $data )
{
  $idList = [];
  if(isset($data->access)) return $data->access;
  else return $idList;
}

function iCheckPath ( $user, $path, $accessIds, &$errorText )
{
  $dir = $path;

  if((gettype($accessIds) == "array") && (count( $accessIds ) > 0)) {
    foreach ( $accessIds as $a ) {
    if(strpos(strtolower($path), strtolower($a)) !== false) {
      //echo $a . " hit " . $path . "\n";
      return 0;}
    }
  }

  // pathinfo is not correct on directories only
  if ( !iIsDir ( $path ) ) {
    $pathParts = pathinfo ( $path );
    $dir = $pathParts['dirname'];
  }

  if ( strstr( $path, '..' ) ) {
    $errorText = "path contains directory traversal";
    return 1;
  }

  // check rights
  if ( $user->tyP != 'u' ) {

    $userWriteablePath = false;

    //echo  ( $user->docDir ). "\n";
    //echo  ( $user->hmDir ). "\n";
    //echo  ( $path ). "\n";

    if ( empty ( $user->docDir ) && empty ( $user->hmDir ) ) {
      $errorText = "exDir and hmDir of user not set";
      return 1;
    }

    if ( !empty ( $user->docDir ) && !strncmp ( $user->docDir, $dir, strlen ( $user->docDir ) ) ) {
      $userWriteablePath = true;
    }

    if ( !empty ( $user->hmDir ) && !strncmp ( $user->hmDir, $dir, strlen ( $user->hmDir ) ) ) {
      $userWriteablePath = true;
    }

    if ( !$userWriteablePath ) {
      $errorText = "path not in docDir or hmDir of user";
      return 1;
    }
  }

  return 0;
}

function iRmDir ( $path ) {
  $d = opendir( $path );

  if ( !$d )
    return false;

  while ( ($f = readdir( $d )) !== false ) {
    if ( $f != '.' && $f != '..' ) {
      $p = $path . '/' . $f;

      if ( is_dir ( $p ) ) {
        if ( !iRmDir ( $p ) ) return false;
      } else {
        if ( !unlink ( $p ) ) return false;
      }
    }
  }

  closedir ( $d );

  if ( !rmdir ( $path ) )
    return false;

  return true;
}

function ixCopy ( $source, $dest ) {
  $d = opendir( $source );

  if ( !$d )
    return false;

  while ( ($f = readdir( $d )) !== false ) {
    if ( $f != '.' && $f != '..' ) {
      $p = $source . '/' . $f;

      $dp = $dest . '/' . $f;

      if ( is_dir ( $p ) ) {
        if ( !mkdir ( $dp ) ) return false;
        if ( !ixCopy ( $p, $dp ) ) return false;
      }
      else
        if ( !copy ( $p, $dp ) ) return false;
    }
  }

  closedir ( $d );

  return true;
}

function iZipDirectory( $input_folder, $output_zip_file ) {
  //https://stackoverflow.com/questions/4914750/how-to-zip-a-whole-folder-using-php

  if (!extension_loaded('zip') || !file_exists($input_folder)) {
    return false;
  }

  // Get real path for our folder
  $rootPath = realpath($input_folder);

  // Initialize archive object
  $zip = new ZipArchive();
  /* Opening the zip file and creating it if it doesn't exist. */
  $zip->open($output_zip_file, ZipArchive::CREATE |
  ZipArchive::OVERWRITE);

  // Create recursive directory iterator
  /** @var SplFileInfo[] $files */
  $files = new RecursiveIteratorIterator(
      new RecursiveDirectoryIterator($rootPath),
      RecursiveIteratorIterator::SELF_FIRST
  );

  foreach ($files as $name => $file)
  {
    // Skip directories (they would be added automatically)
    if (!$file->isDir())
    {
      // Get real and relative path for current file
      $filePath = $file->getRealPath();
      $relativePath = substr($filePath, strlen($rootPath) + 1);

      // Add current file to archive
      $zip->addFile($filePath, $relativePath);
    }
  }

  // Zip archive will be created only after closing object
  return $zip->close();
}

function iWriteFile($fileName, $content) {
  //https://stackoverflow.com/questions/5449395/file-locking-in-php
  //https://stackoverflow.com/questions/20771824/test-if-file-is-locked
  if ( file_exists ( $fileName ) ) {
    if ($fp = fopen($fileName, 'w')) {
      if (!flock($fp, LOCK_EX)){ //another thread is writing -> wait 0.2 sec
        time_nanosleep(0, 200000);
        return file_put_contents( $fileName, $content, LOCK_EX );
      }
      flock($fp, LOCK_UN);
      fclose($fp);
    }
  }
  return file_put_contents( $fileName, $content, LOCK_EX );
}

function iAppendContent($fileName, $content) {
  $res = 0;
  //https://stackoverflow.com/questions/5449395/file-locking-in-php
  //https://stackoverflow.com/questions/20771824/test-if-file-is-locked

  if (! file_exists ( $fileName ) ){
    return file_put_contents( $fileName, '[' . $content . ']');
  }

  if ($fp = fopen($fileName, 'c')) {
    if (!flock($fp, LOCK_EX)){ //another thread is writing -> wait 0.2 sec
      time_nanosleep(0, 200000);

      fseek($fp,-1,SEEK_END);
      $res = fwrite($fp, ',' . $content . ']');
      fclose($fp);
      return res;
    }

    fseek($fp,-1,SEEK_END);
    $res = fwrite($fp, ',' . $content . ']');

    flock($fp, LOCK_UN);
    fclose($fp);
  }

  return $res;
}

function iReadFile($fileName) {
  //https://stackoverflow.com/questions/2236668/file-get-contents-breaks-up-utf-8-characters
  if ( file_exists ( $fileName ) ) {
    if ($fp = fopen($fileName, 'r')) {
      if (!flock($fp, LOCK_EX)){ //another thread is writing
        time_nanosleep(0, 200000);
        return  file_get_contents ( $fileName );
      }
      flock($fp, LOCK_UN);
      fclose($fp);
    }

    $c = file_get_contents ( $fileName );
    return $c;
  }
  else return "";
}

function iFileExistNocase($fileName) {
  static $dirList = [];
  if(file_exists($fileName)) {
    return true;
  }
  $directoryName = dirname($fileName);
  if (!isset($dirList[$directoryName])) {
    $fileArray = glob($directoryName . '/*', GLOB_NOSORT);
    $dirListEntry = [];
    foreach ($fileArray as $file) {
      $dirListEntry[strtolower($file)] = true;
    }
    $dirList[$directoryName] = $dirListEntry;
  }
  return isset($dirList[$directoryName][strtolower($fileName)]);
}

function iCheckFileExistance ($fileName, $allowRemote) {
  $ret = (object) [];
  $ret->isRemote = false;
  $ret->exists = false;

  if(file_exists($fileName)) {
    $ret->exists = true;
    return $ret;
  }
  else if($allowRemote == true){
    $file_headers = @get_headers($fileName);
    if ($file_headers === false) {
      return $ret;
    }
    if (stripos($file_headers[0],"404 Not Found") > 0  || stripos($file_headers[0], "403 Forbidden") > 0 || (stripos($file_headers[0], "302 Found") > 0 && stripos($file_headers[7],"404 Not Found") > 0)) {
      return $ret;
    }
    else {
      $ret->isRemote = true;
      $ret->exists = true;
      return  $ret;
    }
  }
}

function iGetJsonFromFile($fileName, $allowRemote) {
  $ret = (object) [];
  $ret->isRemote = false;
  $ret->data = [];

  $fileCheck = iCheckFileExistance($fileName, $allowRemote);
  $ret->isRemote = $fileCheck->isRemote;

  if($fileCheck->exists == false)
    return $ret;
  else if($fileCheck->isRemote){
    $encName = str_replace(' ','%20',$fileName);
    $c = file_get_contents ( $encName );
    if (substr($c, 0, 3) == "\xEF\xBB\xBF") {
        $c = substr($c, 3);  // Remove BOM
    }
    $ret->data = json_decode ( $c );
  }
  else {
    $c = iReadFile ( $fileName );
    if (!$c)
      $ret->data = [];
    else {

      if (substr($c, 0, 3) == "\xEF\xBB\xBF") {
        $c = substr($c, 3);  // Remove BOM
      }

      $ret->data = json_decode ( $c );
    }
  }

  if($ret->data == null) $ret->data = [];
  return $ret;
}

function iCheckFileExistanceWithCurl ($fileName, $allowRemote) {
  $ret = (object) [];
  $ret->isRemote = false;
  $ret->exists = false;

  if(file_exists($fileName)) {
    $ret->exists = true;
    return $ret;
  }
  else if($allowRemote == true){

		$ch = curl_init($fileName);
		curl_setopt($ch, CURLOPT_NOBODY, true); // We don't need body
    curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0');
		curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); // Follow redirects
		curl_setopt($ch, CURLOPT_TIMEOUT, 10); // Timeout
		curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // If HTTPS
		curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);

		curl_exec($ch);

		$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
		curl_close($ch);

		if ($httpCode == 200) {
      $ret->isRemote = true;
      $ret->exists = true;
      return  $ret;
		} else {
		    return $ret;
		}
  }
}

function iGetJsonFromFileWithCurl($fileName, $allowRemote) {
  $ret = (object) [];
  $ret->isRemote = false;
  $ret->data = [];

  $fileCheck = iCheckFileExistanceWithCurl($fileName, $allowRemote);
  $ret->isRemote = $fileCheck->isRemote;

  if($fileCheck->exists == false) {
    return $ret;
  }
  else if($fileCheck->isRemote){

    $encName = str_replace(' ','%20',$fileName);

    $ch = curl_init($encName);
		curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // We don't need body
		curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); // We don't need body
		curl_setopt($ch, CURLOPT_TIMEOUT, 10); // Timeout
    curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0');

    $response = curl_exec($ch);

    if ($response === false) {
      return $ret;
    }

    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    if ($httpCode !== 200) {
      return $ret;
    }

    $ret->data = json_decode($response, true);
  }
  else {
    $c = iReadFile ( $fileName );
    if (!$c)
      $ret->data = [];
    else
      $ret->data = json_decode ( $c );
  }

  if($ret->data == null) $ret->data = [];
  return $ret;
}

function iAddWorkGroupReference ($groupFileName, $grId, $grNm, $uId, $uNm) {
  $grpList = [];

  if(file_exists( $groupFileName )) {
    $f = iReadFile ( $groupFileName );
    $grpList = json_decode ( $f );
  }

  //add new group entry
  $grpEntry = (object) [];
  $grpEntry->grId = $grId;
  $grpEntry->grNm = $grNm;
  $grpEntry->owner = $uId;
  $grpEntry->ownerName = $uNm;

  $grpList[] = $grpEntry;
  $json = json_encode ( $grpList );

  if ( !iWriteFile( $groupFileName, $json ) ) {
  }

  return true;
}

function iUpdateWorkGroupReference ($groupFileName, $grId, $gTtl) {

  $grpList = [];
  $grpListNew = [];

  if(file_exists( $groupFileName )) {
    $f = iReadFile ( $groupFileName );
    $grpList = json_decode ( $f );
  }

  foreach ( $grpList as $g ) {
    $grpEntry = (object) [];
    $grpEntry->grId = $g->grId;
    $grpEntry->owner = $g->owner;
    $grpEntry->ownerName = $g->ownerName;
    $grpEntry->grNm = $g->grNm;

    if($g->grId == $grId) {
      $grpEntry->grNm = $gTtl;
     }

    $grpListNew[] = $grpEntry;
  }

  $json = json_encode ( $grpListNew );
  if ( !iWriteFile( $groupFileName, $json ) ) {
  }

  return true;
}

function iRemoveWorkGroupReference ($groupFileName, $grId) {

  $grpList = [];
  $grpListNew = [];

  if(file_exists( $groupFileName )) {
    $f = iReadFile ( $groupFileName );
    $grpList = json_decode ( $f );
  }

  foreach ( $grpList as $g ) {
    if($g->grId != $grId) {
      $grpListNew[] = $g;
    }
  }

  $json = json_encode ( $grpListNew );
  if ( !iWriteFile( $groupFileName, $json ) ) {
  }

  return true;
}

function iRemoveDocumentReference ($fileName, $docId) {

  $docList = [];
  $docListNew = [];

  if(file_exists( $fileName )) {
    $f = iReadFile ( $fileName );
    $docList = json_decode ( $f );
  }

  foreach ( $docList as $d ) {
    if($d->dId != $docId) {
      $docListNew[] = $d;
    }
  }

  $json = json_encode ( $docListNew );
  if ( !iWriteFile( $fileName, $json ) ) {
  }

  return true;
}

function iRemoveUserWgReference($fileName, $userId) {

  $userList = [];
  $userListNew = [];

  if(file_exists( $fileName )) {
    $f = iReadFile ( $fileName );
    $userList = json_decode ( $f );
  }

  foreach ( $userList as $u ) {
    if($u->uId != $userId) {
      $userListNew[] = $u;
    }
  }

  $json = json_encode ( $userListNew );
  if ( !iWriteFile( $fileName, $json ) ) {
  }

  return true;
}

function iRemoveDocument($owner, $docId, $mode, $errorText)
{
  $docDirName = $owner->docDir . "/Docs/" . $docId;

  if ( iCheckPath ( $owner, $docDirName, [], $errorText ) ) {
    return 1;
  }

  //remove all group references --------------
  $wgRefFileName =$docDirName . "/workGroups.json";
  $grpList = [];

  if(file_exists( $wgRefFileName )) {
    $f = iReadFile ( $wgRefFileName );
    $grpList = json_decode ( $f );
  }

  //echo ("parsing group list "). "\n";
  foreach ( $grpList as $g ) {
    $groupDocFile = 'Poi3dUserContent/' . $g->owner . '/WorkGroups/' . $g->grId . '/documents.json';
    //echo ("groupDocFile " . $groupDocFile). "\n";
    iRemoveDocumentReference($groupDocFile, $docId);
  }

  //delete document directory
  if($mode == "delete"){
    //update user docs
    $userDocumentsFile = $owner->hmDir . '/documents.json';
    //echo ("userDocumentsFile " . $userDocumentsFile). "\n";
    iRemoveDocumentReference($userDocumentsFile, $docId);

    if ( !iRmDir ( $docDirName ) ) {
      $errorText = "couldn't delete directory";
      return 2;
    }
  }//if($updateOwner == true)

  return 0;
}

function iRemoveWorkGroup($owner, $wgId, $wgDir, $mode, $errorText)
{
  if(empty($wgDir))
  {
    $grpDirName = $owner->docDir . '/WorkGroups/' . $wgId;
    if ( iCheckPath ( $owner, $grpDirName, [], $errorText ) ) {
      return 1;
    }
  }
  else
    $grpDirName = $wgDir;

  //remove workgroup entry from users --------------
  $wgRefFileName =$grpDirName . "/users.json";
  $userList = [];

  if(file_exists( $wgRefFileName )) {
    $f = iReadFile ( $wgRefFileName );
    $userList = json_decode ( $f );
  }

  //echo ("parsing group list "). "\n";
  foreach ( $userList as $u ) {
    $userGroupFile = 'Poi3dUserData/' . $u->uId . '/workGroups.json';
    //echo ($userGroupFile). "\n";
    iRemoveWorkGroupReference($userGroupFile,$wgId);
  }

  //remove workgroup entry from work group documents ------------
  $wgRefFileName =$grpDirName . "/documents.json";
  $docList = [];

  if(file_exists( $wgRefFileName )) {
    $f = iReadFile ( $wgRefFileName );
    $docList = json_decode ( $f );
  }

  if(strpos($grpDirName, 'Poi3dCatalog') !== false) {
    //remove workgroup entry from catalog
    $catEntriesFname = 'Poi3dCatalog/catalog.json';
    iRemoveWorkGroupReference($catEntriesFname,$wgId);
  }
  else {
    foreach ( $docList as $d ) {
      $docGroupFile = 'Poi3dUserContent/' . $d->owner . '/Docs/' . $d->dId . '/workGroups.json';
      //echo ($docGroupFile). "\n";
      iRemoveWorkGroupReference($docGroupFile,$wgId);
    }
  }

  //delete group directory
  if($mode == "delete"){
    if ( !iRmDir ( $grpDirName ) ) {
      $errorText = "couldn't delete directory";
      return 2;
    }
  }//if($updateOwner == true)

  return 0;
}

function iCreateJsonUserList () {

  $srcDir = 'Poi3dUsers';
  $destFile = 'Poi3dUsers/UserList.json';
  $excludeFile = 'UserList';

  $listContent = [];
  $d = opendir( $srcDir );

  if ( !$d )
    return 1;

  while ( ($f = readdir( $d )) !== false ) {
    if ( $f != '.' && $f != '..' ) {
      $p = $srcDir . '/' . $f;

      if ((strpos($p, '.json') !== false) && (strpos($p, '.json.') === false) && (strpos($p, $excludeFile) === false)) {
        //echo ($p). "\n";
        $f = iReadFile ( $p );
        $e = json_decode ( $f );

        //if($e->tyP != 's') {
          //echo ($p). "\n";
          $listEntry = (object) [];
          $listEntry->uId = $e->uId;
          $listEntry->aL = $e->aL;
          $listEntry->tyP = $e->tyP;

          $filename = 'Poi3dUserData/' . $e->uId . '/profile.json';
          //echo ($e->aL). " ".($e->uId). " " . ($filename). "\n";

          if(file_exists($filename)) {
            $udf = iReadFile ( $filename );
            $udo = json_decode ( $udf );

            $listEntry->fN = $udo->fN;
            $listEntry->lN = $udo->lN;
            $listEntry->pT = $udo->pT;

            //echo ($listEntry->aL). " " . ($filename). "\n";

            $listContent[] = $listEntry;
          }
        //}//if($e->tyP != 's') {
      }
    }
  }

  closedir ( $d );
  $json = json_encode ( $listContent );

  if ( !iWriteFile( $destFile, $json ) ) {
    return false;
  }

  return true;
}

// returns 0 if $ip was already used in last hours
// returns 1 if $ip is allowed to register
// returns 2 if an error occured
// IPs older than defined time will be removed
function iCheckIp ( $ip, &$errorText ) {
  $errorText = '';

  $file = 'reglist.json';
  $regdata = [];

  if ( file_exists ( $file ) ) {
    $json = iReadFile ( $file );

    if ( $json === false ) {
      $errorText = "couldn't read file";
      return 2;
    }

    $regdata = json_decode ( $json );
    if ( $regdata == NULL ) {
      $errorText = "couldn't decode json";
      return 2;
    }
  }

  //date_default_timezone_set('UTC'); //shouldn't be relevant if used on one system, might change other functions as well
  $block_time = 1*10*60;

  $regdata_new = [];

  // remove old entries; build new list with currently blocked IPs
  foreach ( $regdata as $k => $v ) {

    if ( (intval($v->time) + $block_time) > time() ) {
      // keep entry
      $regdata_new[] = $v;
    }
  }

  $ip_blocked = false;
  $ip_found = false;

  // check if ip in list and increase counter
  foreach ( $regdata_new as $k => $v ) {
    if ( $v->ip == $ip ) {
      $ip_found = true;
      $c = 0;
      if ( isset( $v->count ) ) {
        $c = intval($v->count);
      }

      if ( $c >= 3 ) {
        $ip_blocked = true;
      } else {
        $v->count = $c + 1;
      }
    }
  }

  // add ip if not found before
  if ( !$ip_found ) {
    $regdata_new[] = (object) [ 'ip' => $ip, 'time' => time(), 'count' => 1 ];
  }

  $json = json_encode ( $regdata_new );
  if ( iWriteFile ( $file, $json ) === FALSE ) {
    $errorText = "couldn't create file";
    return 2;
  }

  // chmod ( $file, 0666 ); // just used for testing on dev.web.home.c...

  if ( $ip_blocked ) {
    return 0;
  }

  // new IP saved to list
  return 1;
}

function iPushMessageToUsers ($senderAlias, $recipients, $actionType, $docId, $grpId, $msgText, $objectData, &$errorText)
{
  $crypto = (object) [];
  $vapid = (object) [];
  $errorText = "";

  if(iLoadCryptoKeys($crypto, $vapid) == false) {
    $errorText = 'unknown configuration';
    return 1;
  }

  //echo ("aL=" . $senderAlias ." aCt=". $actionType ." dId=". $docId ." tXt=". $msgText." recip=". json_encode($recipients)). "\n";

  //prepare message
  $date = new DateTime();
  $dateEntry = $date->setTimezone(new \DateTimeZone('UTC'))->format(DateTime::ATOM);

  $ntfMsg = (object) [];
  $ntfMsg->aL = $senderAlias;
  $ntfMsg->dId = $docId;
  $ntfMsg->gId = $grpId;
  $ntfMsg->cDt = $dateEntry;
  $ntfMsg->aCt = $actionType;
  $ntfMsg->tXt = $msgText;

  if(($ntfMsg->aCt == 'incomingCall')||($ntfMsg->aCt == 'callAccepted')||($ntfMsg->aCt == 'testMessage')){
    $ntfMsg->rCp = $recipients;
    $ntfMsg->oDt = (object) [];
  }
  else
    $ntfMsg->oDt = $objectData;

  $ntfJsonMsg = json_encode ( $ntfMsg );

  $pnVapid = new PNVapid( $vapid->subject, $vapid->publicKey, $vapid->privateKey ) ;
  $pnServer = new PNServer();

  $pnServer->setVapid( $pnVapid );
  $pnServer->setPayload( $ntfJsonMsg );

  $responseCount400 = 0;  // invalid request count, iPushMessageToUsers returns 4 if all pushmessages fail
  $responseCount429 = 0;  // too many requests count, iPushMessageToUsers returns 5 if all push messages fail

  foreach ( $recipients as $u ) {
    $loginFlNm = 'SignedUsers/' . $u->aL . '.json';
    if (file_exists($loginFlNm)) {
      $f = iReadFile ( $loginFlNm );
      $fContent = json_decode ( $f );
      $endpoints = $fContent->endpoints;

      if($ntfMsg->aCt == 'incomingCall'){
        $fContent->callOffer = $objectData;
        $fContent_json = json_encode( $fContent );
        if ( iWriteFile( $loginFlNm, $fContent_json ) === false ) {
          $errorText = "couldn't save call offer";
          return 3;
        }
      }
      else if($ntfMsg->aCt == 'callAccepted'){
        $fContent->callAnswer = $objectData;
        $fContent_json = json_encode( $fContent );
        if ( iWriteFile( $loginFlNm, $fContent_json ) === false ) {
          $errorText = "couldn't save call answer";
          return 3;
        }
      }
      $endpoints_new = [];
      $endpoint_removed = false;

      foreach ( $endpoints as $endpoint ) {
        $subscription = new PNSubscription( $endpoint->endpoint, $endpoint->key, $endpoint->auth, 0, $endpoint->contentEncoding );

        if ( !$pnServer->pushSingle( $subscription ) ) {
          // problem with VAPID
          $errorText .= "Pushmessage not sent. Issue with VAPID.";
          return 1;
        }

        // check for problems on endpoints
        $log = $pnServer->getLog(); // log is appended on each pushSingle()
        foreach ( $log as $k => $v ) {

          // just check current endpoint
          if ( $k == $endpoint->endpoint ) {
            $endpointTail = substr( $k, -7 );
            switch ( $v['curl_response_code'] ) {
              case 201:
                // success
                $endpoints_new[] = $endpoint;
                break;
              case 400:
                $responseCount400++;
                $endpoints_new[] = $endpoint;
                break;
              case 404: // subscription expired / not found
              case 410: // subscription gone
                $endpoint_removed = true;
                break;
              case 413:
                $errorText .= "Payload too large for endpoint ...{$endpointTail}.";
                return 6;
              case 429:
                $responseCount429++;
                $endpoints_new[] = $endpoint;
                break;
              default:
                $errorText .= "Unknown reponse code {$v['curl_response_code']} for endpoint ...{$endpointTail}.";
                return 7;
            }

          }
        }
        // $errorText .= print_r ( $pnServer->getLog(), true ) . "\r\n\r\n";
      }

      if ( $endpoint_removed ) {
        // if at least one endpoint of a user is removed, endpoint file is rewritten

        $fContent->endpoints = $endpoints_new;
        $fContent_json = json_encode( $fContent );
        if ( iWriteFile( $loginFlNm, $fContent_json ) === false ) {
          $errorText = "couldn't save new endpoints";
          return 8;
        }

      }
    }
  }

  if ( $responseCount400 == count( $recipients ) ) {
    $errorText .= "All endpoints responded with code 400 (invalid request).";
    return 4;
  }

  if ( $responseCount429 == count( $recipients ) ) {
    $errorText .= "All endpoints responded with code 429 (too many requests).";
    return 5;
  }

  return 0;
}

function iGenerateSymmetricalKeys ( &$pbKey, &$prKey, &$errorText )
{
  try {
    $keypair = sodium_crypto_box_keypair();
    $pbKey = sodium_bin2hex( sodium_crypto_box_publickey( $keypair ) );
    $prKey = sodium_bin2hex( sodium_crypto_box_secretkey( $keypair ) );
  } catch ( SodiumException $e ) {
    $errorText = "Error creating new symmetrical keypair: " . $e->getMessage();
    return false;
  }

  return true;
}

function DeleteOutputFile ( $data, &$ret, &$errorText )
{
  $ret = [];
  $errorText = "";

  if ( empty ( $data->fileName ) ) {
    $errorText = "invalid data structure";
    return 1;
  }

  $filename = $data->fileName;
  //echo ("filename " . $filename). "\n";

  if (strpos($filename, 'FileOutput') === false) {
    $errorText = "access not granted";
    return 2;
  }

  if(!file_exists($filename)) {
    $errorText = "file does not exist";
    return 3;
  }

  unlink($filename);
  return 0;
}

function ReadPortalConfiguration ( $data, &$ret, &$errorText, &$srvInfo )
{
  $ret = [];
  $errorText = "";
  $srvInfo = (object)[];
  $simpleRep = true;

  $filename = 'portalConfig.json';

  if(!file_exists($filename)) {

    $fContent = (object) [];
    $fContent->encComm = false;
    $fContent->encAtt = 0;
    $fContent->mlHost = "";
    $fContent->mlUser = "";
    $fContent->mlPwd = "";
    $fContent->mlPort = "";
    $fContent->mlFrom = "";
    $fContent->mlFromTtl = "";
    $fContent->mlFrom2fa = "";
    $fContent->mlFrom2faTtl = "";
    $fContent->mlBcc = "";
    $fContent->bgTxt = "";
    $fContent->helpDir = "https://mypoi3d.com/Videos/Help";
    $fContent->demoDocDir = "https://mypoi3d.com/Poi3dSamples";
    $fContent->downloadDir = "https://mypoi3d.com/Downloads";
    $fContent->useFlags = [1,1,1,1,1,1];

    $fContent->prEncCommKey = "";
    $fContent->pbEncCommKey = "";
    iGenerateSymmetricalKeys( $fContent->pbEncCommKey, $fContent->prEncCommKey, $errorText );

    $fContent->prPushKey = "";
    $fContent->pbPushKey = "";

    $json = json_encode ( $fContent );
    iWriteFile( $filename, $json );
  }

  $f = iReadFile ( $filename );

  if ( !$f ) {
    $errorText = "couldn't open " . $filename;
    return 2;
  }

  $fContent = json_decode ( $f );

  if(empty ( $fContent->prEncCommKey ) || (empty ( $fContent->pbEncCommKey )))
    $fContent->encComm = false;

  //check for empty user directory
  $fCount = count(glob('./Poi3dUsers/' . "*.json"));
  if($fCount > 1) $fContent->uCnt = $fCount - 1; //UserList
  else $fContent->uCnt = $fCount;

  if(!empty ( $data->iD )) {
    if (! iLoadUserBaseData( $data, $user, $errorText ) ) {
        if( $user->tyP == 's' )
          $simpleRep = false;
    }
  }
  $errorText = "";

  //delete private keys
  unset($fContent->prPushKey);
  unset($fContent->prEncCommKey);

  if($simpleRep == true) {
    unset($fContent->mlHost);
    unset($fContent->mlUser);
    unset($fContent->mlPwd);
    unset($fContent->mlPort);
    unset($fContent->mlFrom);
    unset($fContent->mlFromTtl);
    unset($fContent->mlFrom2fa);
    unset($fContent->mlFrom2faTtl);
    unset($fContent->mlBcc);
    if(!empty ( $fContent->aiBots )) {
      foreach ( $fContent->aiBots as $ab ) {
        unset($ab->key);
      }
    }
  }

  $ret = $fContent;

  return 0;
}

function WritePortalConfiguration ( $data, &$ret, &$errorText, &$srvInfo )
{
  $ret = [];
  $srvInfo = (object)[];
  $errorText = "";

  if ( iLoadUserBaseData( $data, $admin, $errorText ) ) {
    $errorText = "invalid user";
    return 1;
  }

  //check admin is calling
  if ( $admin->tyP != 's' ) {
    $errorText = "no admin rights";
    return 2;
  }

  $filename = 'portalConfig.json';

  if(!file_exists($filename)) {
    $errorText = "file does not exist";
    return 3;
  }

  $f = iReadFile ( $filename );
  $fContent = json_decode ( $f );
  $dContent = $data->data;

  if(isset( $dContent->encComm)) $fContent->encComm = $dContent->encComm;
  if(isset( $dContent->encAtt)) $fContent->encAtt = $dContent->encAtt;
  if(isset( $dContent->mlHost)) $fContent->mlHost = $dContent->mlHost;
  if(isset( $dContent->mlUser)) $fContent->mlUser = $dContent->mlUser;
  if(isset( $dContent->mlPwd)) $fContent->mlPwd = $dContent->mlPwd;
  if(isset( $dContent->mlPort)) $fContent->mlPort = $dContent->mlPort;
  if(isset( $dContent->mlFrom)) $fContent->mlFrom = $dContent->mlFrom;
  if(isset( $dContent->mlFromTtl)) $fContent->mlFromTtl = $dContent->mlFromTtl;
  if(isset( $dContent->mlFrom2fa)) $fContent->mlFrom2fa = $dContent->mlFrom2fa;
  if(isset( $dContent->mlFrom2faTtl)) $fContent->mlFrom2faTtl = $dContent->mlFrom2faTtl;
  if(isset( $dContent->mlBcc)) $fContent->mlBcc = $dContent->mlBcc;
  if(isset( $dContent->bgTxt)) $fContent->bgTxt = $dContent->bgTxt;
  if(isset( $dContent->helpDir)) $fContent->helpDir = $dContent->helpDir;
  if(isset( $dContent->downloadDir)) $fContent->downloadDir = $dContent->downloadDir;
  if(isset( $dContent->demoDocDir)) $fContent->demoDocDir = $dContent->demoDocDir;
  if(isset( $dContent->usrRights)) $fContent->usrRights = $dContent->usrRights;
  if(isset( $dContent->attRights)) $fContent->attRights = $dContent->attRights;
  if(isset( $dContent->extCtlgs)) $fContent->extCtlgs = $dContent->extCtlgs;
  if(isset( $dContent->aiBots)) $fContent->aiBots = $dContent->aiBots;

  if(isset( $dContent->pbPushKey)) $fContent->pbPushKey = $dContent->pbPushKey;
  if(isset( $dContent->prPushKey)) $fContent->prPushKey = $dContent->prPushKey;

  //echo ("fContent " . json_encode ($fContent)). "\n";

  $json = json_encode ( $fContent );
  if ( !iWriteFile( $filename, $json )) {
    $errorText = "couldn't write file";
    return 4;
  }

  return 0;
}

function ReadJsonFile ( $data, &$ret, &$errorText, &$srvInfo )
{
  $ret = [];
  $errorText = "";
  $srvInfo = (object)[];
  $globalAccess = false;

  if(iCheckCaller() == false) {
    $errorText = "invalid call";
    return 1;
  }

  if ( empty ( $data->fileName ) ) {
    $errorText = "invalid data structure";
    return 1;
  }
  $filename = $data->fileName;

  $fileCheck = iCheckFileExistance($filename, true);
  if(($fileCheck->exists == false)&&(strpos(strtolower($filename), 'userlist.json') !== false))
  {
    iCreateJsonUserList();
    $fileCheck = iCheckFileExistance($filename, true);
  }

  if($fileCheck->exists == false) {
    $errorText = "file does not exist";
    return 2;
  }

  if (strpos(strtolower($filename), 'portalconfig.json') !== false) {
    $errorText = "please use ReadPortalConfiguration";
    return 3;
  }

  $globalAccess = iCheckGlobalAccess($filename);

  if ($globalAccess == false){
    if ( iLoadUserBaseData( $data, $user, $errorText ) ) {
      return 4;
    }

    if (strpos(strtolower($filename), 'userlist.json') == false) {
      if ( iCheckPath ( $user, $filename, iGetAccessIds($data), $errorText ) ) {
        return 5;
      }
    }
  }

  $content = iGetJsonFromFile($filename,true);
  if(($content->isRemote == false)&&(!empty($data->checkDate))) {
    $srvInfo->modDate = date(DATE_ATOM, filemtime($filename));

    $clientDate = new DateTime($data->checkDate);
    $serverDate = new DateTime($srvInfo->modDate);

    if ($serverDate <= $clientDate) {
      return 0;
    }
  }
  else {
    $srvInfo->modDate = date(DATE_ATOM, time());
  }
  $ret = $content->data;

  return 0;
}

function WriteJsonFile ( $data, &$ret, &$errorText, &$srvInfo )
{
  $ret = [];
  $srvInfo = (object)[];
  $errorText = "";

  if(iCheckCaller() == false) {
    $errorText = "invalid call";
    return 1;
  }

  if ( empty ( $data->fileName ) || empty ( $data->data ) ) {
    $errorText = "invalid data structure";
    return 1;
  }

  if ( iLoadUserBaseData( $data, $user, $errorText ) ) {
    return 2;
  }

  $srvInfo->fNm = $data->fileName;
  $srvInfo->lockUser = "";

  if ( iCheckPath ( $user, $data->fileName, iGetAccessIds($data), $errorText ) ) {
    return 3;
    }

  $pathParts = pathinfo ( $data->fileName );

  if ( strcmp ( strtolower( $pathParts['extension'] ), 'json' ) ) {
    $errorText = "only json files supported";
    return 4;
  }

  if ( !file_exists( $pathParts['dirname'] ) || !is_dir ( $pathParts['dirname'] ) ) {
    // this doesn't support sub dirs
    if ( !mkdir ( $pathParts['dirname'] ) ) {
      $errorText = "couldn't create directory";
      return 5;
    }
  }

  //check lock
  $lockUser = "";
  if ( iCheckLock ( $user, $data->fileName, $errorText, $lockUser)) {
    $srvInfo->lockUser = $lockUser;
    return 9;
  }

  if ( !iWriteFile( $data->fileName, $data->data )) {
    $errorText = "couldn't write file";
    return 7;
  }

  //notify group users
  if (!empty ( $data->recipients )  && is_array ( $data->recipients ) ) {
      $ntfObjData = (object) [];
      if (strpos($pathParts['filename'], 'annotations') !== false){
      iPushMessageToUsers ($user->aL, $data->recipients, "annChanged", $data->docId, "", "Annotations changed from user " . $user->aL, $ntfObjData, $errorText);
      }
      else if (strpos($pathParts['filename'], 'locations') !== false){
      iPushMessageToUsers ($user->aL, $data->recipients, "locChanged", $data->docId, "", "View locations changed from user " . $user->aL, $ntfObjData, $errorText);
      }
      else if (strpos($pathParts['filename'], 'instructions') !== false){
      iPushMessageToUsers ($user->aL, $data->recipients, "instrChanged", $data->docId, "", "Instructions changed from user " . $user->aL, $ntfObjData, $errorText);
      }
      else if (strpos($pathParts['filename'], 'materialSets') !== false){
      iPushMessageToUsers ($user->aL, $data->recipients, "mSetsChanged", $data->docId, "", "Material sets changed from user " . $user->aL, $ntfObjData, $errorText);
      }
      else if (strpos($pathParts['filename'], 'fileAttachments') !== false){
      iPushMessageToUsers ($user->aL, $data->recipients, "fAttChanged", $data->docId, "", "File attachments changed from user " . $user->aL, $ntfObjData, $errorText);
      }
      else if (strpos($pathParts['filename'], 'boms') !== false){
      iPushMessageToUsers ($user->aL, $data->recipients, "bomsChanged", $data->docId, "", "BOMs changed from user " . $user->aL, $ntfObjData, $errorText);
      }
      else if (strpos($pathParts['filename'], 'sensors') !== false){
      iPushMessageToUsers ($user->aL, $data->recipients, "sensorsChanged", $data->docId, "", "Sensor collection changed from user " . $user->aL, $ntfObjData, $errorText);
      }
      else if (strpos($pathParts['filename'], 'docChat') !== false){
      iPushMessageToUsers ($user->aL, $data->recipients, "docChatChanged", $data->docId, "", "New chat message from user " . $user->aL, $ntfObjData, $errorText);
      }
      else if (strpos($pathParts['filename'], 'grpChat') !== false){
      iPushMessageToUsers ($user->aL, $data->recipients, "grpChatChanged", "", $data->grpId, "New chat message from user " . $user->aL, $ntfObjData, $errorText);
      }
      else if (strpos($pathParts['filename'], 'docAttributes') !== false){
      iPushMessageToUsers ($user->aL, $data->recipients, "docAttrChanged", $data->docId, "", "Document attributes changed from user " . $user->aL, $ntfObjData, $errorText);
      }
  }//if (!empty ( $data->recipients ) )
  return 0;
}

function AppendToJsonFile ( $data, &$ret, &$errorText, &$srvInfo )
{
  $ret = [];
  $srvInfo = (object)[];
  $errorText = "";

  if(iCheckCaller() == false) {
    $errorText = "invalid call";
    return 1;
  }

  if ( empty ( $data->fileName ) || empty ( $data->data )) {
    $errorText = "invalid data structure";
    return 1;
  }

  if ( iLoadUserBaseData( $data, $user, $errorText )) {
    return 2;
  }

  $srvInfo->fNm = $data->fileName;
  $srvInfo->lockUser = "";

  //check lock
  $lockUser = "";
  if ( iCheckLock ( $user, $data->fileName, $errorText, $lockUser)) {
    $srvInfo->lockUser = $lockUser;
    return 3;
  }

  if ( iCheckPath ( $user, $data->fileName, iGetAccessIds($data), $errorText ) ) {
    return 4;
    }

  $pathParts = pathinfo ( $data->fileName );

  if ( !iAppendContent( $data->fileName, $data->data )) {
    $errorText = "couldn't write file";
    return 5;
  }

  //notify group users
  if (!empty ( $data->recipients )  && is_array ( $data->recipients ) ) {
      $ntfObjData = (object) [];
      if (strpos($pathParts['filename'], 'annotations') !== false){
      iPushMessageToUsers ($user->aL, $data->recipients, "annChanged", $data->docId, "", "Annotations changed from user " . $user->aL, $ntfObjData, $errorText);
      }
      else if (strpos($pathParts['filename'], 'locations') !== false){
      iPushMessageToUsers ($user->aL, $data->recipients, "locChanged", $data->docId, "", "View locations changed from user " . $user->aL, $ntfObjData, $errorText);
      }
      else if (strpos($pathParts['filename'], 'instructions') !== false){
      iPushMessageToUsers ($user->aL, $data->recipients, "instrChanged", $data->docId, "", "Instructions changed from user " . $user->aL, $ntfObjData, $errorText);
      }
      else if (strpos($pathParts['filename'], 'materialSets') !== false){
      iPushMessageToUsers ($user->aL, $data->recipients, "mSetsChanged", $data->docId, "", "Material sets changed from user " . $user->aL, $ntfObjData, $errorText);
      }
      else if (strpos($pathParts['filename'], 'fileAttachments') !== false){
      iPushMessageToUsers ($user->aL, $data->recipients, "fAttChanged", $data->docId, "", "File attachments changed from user " . $user->aL, $ntfObjData, $errorText);
      }
      else if (strpos($pathParts['filename'], 'boms') !== false){
      iPushMessageToUsers ($user->aL, $data->recipients, "bomsChanged", $data->docId, "", "BOMs changed from user " . $user->aL, $ntfObjData, $errorText);
      }
      else if (strpos($pathParts['filename'], 'sensors') !== false){
      iPushMessageToUsers ($user->aL, $data->recipients, "sensorsChanged", $data->docId, "", "Sensor collection changed from user " . $user->aL, $ntfObjData, $errorText);
      }
      else if (strpos($pathParts['filename'], 'docChat') !== false){
      iPushMessageToUsers ($user->aL, $data->recipients, "docChatChanged", $data->docId, "", "New chat message from user " . $user->aL, $ntfObjData, $errorText);
      }
      else if (strpos($pathParts['filename'], 'grpChat') !== false){
      iPushMessageToUsers ($user->aL, $data->recipients, "grpChatChanged", "", $data->grpId, "New chat message from user " . $user->aL, $ntfObjData, $errorText);
      }
      else if (strpos($pathParts['filename'], 'docAttributes') !== false){
      iPushMessageToUsers ($user->aL, $data->recipients, "docAttrChanged", $data->docId, "", "Document attributes changed from user " . $user->aL, $ntfObjData, $errorText);
      }
  }//if (!empty ( $data->recipients ) )

  return 0;
}

function LockFile ( $data, &$ret, &$errorText, &$srvInfo )
{
  $ret = [];
  $srvInfo = (object)[];
  $errorText = "";

  if ( empty ( $data->fileName )) {
    $errorText = "invalid data structure";
    return 1;
  }
  $filename = $data->fileName;

  if ( iLoadUserBaseData( $data, $user, $errorText ) ) {
    return 2;
  }

  $srvInfo->lockUser = "";

  if ( iCheckPath ( $user, $filename, iGetAccessIds($data), $errorText ) ) {
    return 3;
  }

  $lockUser = "";
  if ( iCheckLock ( $user, $filename, $errorText, $lockUser ) ) {
    $srvInfo->lockUser = $lockUser;
    return 4;
  }

  if ( !file_exists( $filename )) {
    $errorText = "file does not exist";
    return 0;
  }

  $lckFlNm = $filename . '_lock';
  $content = new stdClass();
  $content->lckUserId = $user->uId;
  $content->lckUserNm = $user->aL;
  $json = json_encode ( $content );

  if ( !iWriteFile( $lckFlNm, $json ) ) {
    $errorText = "couldn't lock file";
    return 6;
  }

  $pathParts = pathinfo ( $filename );

  //notify group users
  if (!empty ( $data->recipients )  && is_array ( $data->recipients ) ) {
      $ntfObjData = (object) [];

      if (strpos($pathParts['filename'], 'annotations') !== false){
      iPushMessageToUsers ($user->aL, $data->recipients, "annLocked", $data->docId, "", "Annotations locked by user " . $user->aL, $ntfObjData, $errorText);
      }
      else if (strpos($pathParts['filename'], 'locations') !== false){
      iPushMessageToUsers ($user->aL, $data->recipients, "locLocked", $data->docId, "", "View locations locked by user " . $user->aL, $ntfObjData, $errorText);
      }
      else if (strpos($pathParts['filename'], 'instructions') !== false){
      iPushMessageToUsers ($user->aL, $data->recipients, "instrLocked", $data->docId, "", "Instructions locked by user " . $user->aL, $ntfObjData, $errorText);
      }
      else if (strpos($pathParts['filename'], 'materialSets') !== false){
      iPushMessageToUsers ($user->aL, $data->recipients, "mSetsLocked", $data->docId, "", "Material sets locked by user " . $user->aL, $ntfObjData, $errorText);
      }
      else if (strpos($pathParts['filename'], 'fileAttachments') !== false){
      iPushMessageToUsers ($user->aL, $data->recipients, "fAttLocked", $data->docId, "", "File attachments locked by user " . $user->aL, $ntfObjData, $errorText);
      }
      else if (strpos($pathParts['filename'], 'boms') !== false){
      iPushMessageToUsers ($user->aL, $data->recipients, "bomsLocked", $data->docId, "", "BOMs locked by user " . $user->aL, $ntfObjData, $errorText);
      }
      else if (strpos($pathParts['filename'], 'sensors') !== false){
      iPushMessageToUsers ($user->aL, $data->recipients, "sensorsLocked", $data->docId, "", "Sensor collection locked by user " . $user->aL, $ntfObjData, $errorText);
      }
      else if (strpos($pathParts['filename'], 'docAttributes') !== false){
      iPushMessageToUsers ($user->aL, $data->recipients, "docAttrLocked", $data->docId, "", "Document attributes locked by user " . $user->aL, $ntfObjData, $errorText);
      }
  }//if (!empty ( $data->recipients ) )

  return 0;
}

function UnlockFile ( $data, &$ret, &$errorText, &$srvInfo )
{
  $ret = [];
  $srvInfo = (object)[];
  $errorText = "";
  $unlockSuccess = false;

  if ( empty ( $data->fileName )) {
    $errorText = "invalid data structure";
    return 1;
  }
  $filename = $data->fileName;

  if ( iLoadUserBaseData( $data, $user, $errorText ) ) {
    return 2;
  }

  $srvInfo->lockUser = "";

  $lckFlNm = $data->fileName . '_lock';

  if ( !file_exists( $lckFlNm )) {
    return 0;
  }

  $lockUser = "";
  if ( iCheckPath ( $user, $data->fileName, [], $errorText ) == 0) {//owner -> unlock always allowed
    unlink($lckFlNm);
    $unlockSuccess = true;
  }
  else if ( iCheckLock ( $user, $data->fileName, $errorText, $lockUser ) == 0) { //non owner -> check locker
    unlink($lckFlNm);
    $unlockSuccess = true;
  }

  if($unlockSuccess == false) {
    $srvInfo->lockUser = $lockUser;
    return 3;
  }

  $pathParts = pathinfo ( $data->fileName );

  //notify group users
  if (!empty ( $data->recipients )  && is_array ( $data->recipients ) ) {
    $ntfObjData = (object) [];

    if (strpos($pathParts['filename'], 'annotations') !== false){
      iPushMessageToUsers ($user->aL, $data->recipients, "annLocked", $data->docId, "", "Annotations unlocked by user " . $user->aL, $ntfObjData, $errorText);
    }
    else if (strpos($pathParts['filename'], 'locations') !== false){
      iPushMessageToUsers ($user->aL, $data->recipients, "locLocked", $data->docId, "", "View locations unlocked by user " . $user->aL, $ntfObjData, $errorText);
    }
    else if (strpos($pathParts['filename'], 'instructions') !== false){
      iPushMessageToUsers ($user->aL, $data->recipients, "instrLocked", $data->docId, "", "Instructions unlocked by user " . $user->aL, $ntfObjData, $errorText);
    }
    else if (strpos($pathParts['filename'], 'materialSets') !== false){
      iPushMessageToUsers ($user->aL, $data->recipients, "mSetsLocked", $data->docId, "", "Material sets unlocked by user " . $user->aL, $ntfObjData, $errorText);
    }
    else if (strpos($pathParts['filename'], 'fileAttachments') !== false){
      iPushMessageToUsers ($user->aL, $data->recipients, "fAttLocked", $data->docId, "", "File attachments unlocked by user " . $user->aL, $ntfObjData, $errorText);
    }
    else if (strpos($pathParts['filename'], 'boms') !== false){
      iPushMessageToUsers ($user->aL, $data->recipients, "bomsLocked", $data->docId, "", "BOMs unlocked by user " . $user->aL, $ntfObjData, $errorText);
    }
    else if (strpos($pathParts['filename'], 'sensors') !== false){
      iPushMessageToUsers ($user->aL, $data->recipients, "sensorsLocked", $data->docId, "", "Sensor collection  unlocked by user " . $user->aL, $ntfObjData, $errorText);
    }
    else if (strpos($pathParts['filename'], 'docAttributes') !== false){
      iPushMessageToUsers ($user->aL, $data->recipients, "docAttrLocked", $data->docId, "", "Document attributes unlocked by user " . $user->aL, $ntfObjData, $errorText);
    }
  }//if (!empty ( $data->recipients ) )

  return 0;
}

function SaveFileAttachments  ( $data, &$ret, &$errorText, &$srvInfo )
{
  //$srvInfo is created in WriteJsonFile
  $iRet = WriteJsonFile( $data, $ret, $errorText, $srvInfo );

  if ( $iRet != 0 ) {
    return $iRet;
  }

  $ret = [];
  $errorText = "";

  //cleanup Files directory
  $pathParts = pathinfo ( $data->fileName );
  $srcDir = $pathParts['dirname'].'/Files';
  //echo ($srcDir."\n");

  $d = opendir( $srcDir );

  if ( !$d )
    return 0;

  $files = [];
  while ( ($f = readdir( $d )) !== false ) {
    if ( $f != '.' && $f != '..' ) {
      if (strpos($f, '.json') === false) {
        $files[] = $f;
        //echo($f." added\n");
      }
    }
  }
  closedir ( $d );

  $fAtts = json_decode ( $data->data );
  $attNames = [];
  foreach ($fAtts as $item)
  {
    $attNames[] = $item->faId;
    //echo($item->faId." added\n");
  }

  foreach ($files as $fItem)
  {
    if(!in_array($fItem, $attNames))
    {
      //echo ("delete " . $srcDir . '/' . $fItem ."\n");
      unlink ($srcDir . '/' . $fItem);
    }
  }
  return 0;
}

function GetTimestamp ($data, &$ret, &$errorText)
{
  $ret = (object)[];
  $errorText = "";

  $date = new DateTime();
  $ret->srvTs = $date->setTimezone(new \DateTimeZone('UTC'))->format(DateTime::ATOM);

  return 0;
}

//explicit Admin functions
function UpdateUserAccessDataByAdmin ( $data, &$ret, &$errorText )
{
  $ret = (object) [];
  $errorText = "";
  //std user: "{"iD":{"uNm":"TestReader","pWd":"5674657","uId":"gkjgjhg"}}"
  //admin override: "{"iD":{"uNm":"TestReader","pWd":"5674657","aNm":"Poi3dAdmin","uId":"gkjgjhg"}}"

  if ( iLoadUserBaseData( $data, $user, $errorText ) ) {
    $errorText = "invalid user";
    return 1;
  }

  //check admin is calling
  if (( $user->tyP != 's' )&&(empty ( $data->iD->aNm ) ) ){
    $errorText = "invalid admin";
    return 2;
  }

  if (empty ( $data->users ) || (gettype($data->users) != "array") || empty ( $data->nDoc ) || empty ( $data->nWg ) || empty ( $data->nFatt ) || empty ( $data->aExpDt ) || empty ( $data->tyP ) || !isset ( $data->lck )) {
    $errorText = "invalid information given";
    return 3;
  }

  foreach ( $data->users as $u ) {

    $baseFileName = 'Poi3dUsers/' . $u->aL . '.json';

    if(file_exists( $baseFileName )) {
      $f = iReadFile ( $baseFileName );
      $uBaseInfo = json_decode ( $f );
    }
    else {
      $errorText = "couldn't read user base data";
      return 4;
    }

    //update BaseData (tyP)
    $uBaseInfo->tyP = $data->tyP;
    $uBaseInfo->lck = $data->lck;

    $json = json_encode ( $uBaseInfo );
    if ( !iWriteFile ( $baseFileName, $json ) ) {
      $errorText = "couldn't update user base data";
      return 5;
    }

    $profileFileName = $uBaseInfo->hmDir . '/profile.json';

    //update profile.json
    if ( iCheckPath ( $uBaseInfo, $profileFileName, iGetAccessIds($data), $errorText ) ) {
      return 6;
    }

    $f = iReadFile ( $profileFileName );

    if ( !$f ) {
      $errorText = "couldn't open " . $profileFileName;
      return 7;
    }

    $uDetails = json_decode ( $f );

    if (! empty( $data->aExpDt) ) {

      $checkDate = new DateTime($data->aExpDt);
      $year = $checkDate->format('Y');

      if($year == 2000) {
        $uDetails->tyPS = $user->tyP;
        $uDetails->nDcS = $uDetails->nDoc;
        $uDetails->nWgS = $uDetails->nWg;
        $uDetails->nFaS = $uDetails->nFatt;
      }
    }

    $uDetails->nDoc = $data->nDoc;
    $uDetails->nWg = $data->nWg;
    $uDetails->nFatt = $data->nFatt;
    $uDetails->aExpDt = $data->aExpDt;
    $json = json_encode ( $uDetails );

    if ( !iWriteFile ( $profileFileName, $json ) ) {
      $errorText = "couldn't update user details";
      return 8;
    }
  }//foreach ( $data->users as $u )
  return 0;
}

function UpdateUserTypeByAdmin ( $data, &$ret, &$errorText )
{
  $ret = (object) [];
  $errorText = "";

  if ( iLoadUserBaseData( $data, $admin, $errorText ) ) {
    $errorText = "invalid user";
    return 1;
  }

  //check admin is calling
  if ( $admin->tyP != 's' ) {
    $errorText = "no admin rights";
    return 2;
  }

  //update username.json
  $filename = 'Poi3dUsers/' . $data->uNm . '.json';

  $f = iReadFile ( $filename );

  if ( !$f ) {
    $errorText = "couldn't open " . $filename;
    return 3;
  }

  $user = json_decode ( $f );

  //update BaseData (tyP)
  $user->tyP = $data->tyP;
  $json = json_encode ( $user );

  if ( !iWriteFile ( $filename, $json ) ) {
    $errorText = "couldn't update user base data";
    return 4;
  }

  return 0;
}

function CreateUserList ( $data, &$ret, &$errorText )
{
  $ret = [];
  $errorText = "";

  if (! iCreateJsonUserList() ) {
    return 1;
  }
  return 0;
}

function UpdatePwd ( $data, &$ret, &$errorText )
{
  $ret = [];
  $errorText = "";

  if ( iLoadUserBaseData( $data, $user, $errorText ) ) {
    return 1;
  }

  // remove temporar pwd if used
  unset( $user->tmpPwd );
  unset( $user->tmpPwdTimestamp );

  $user->pWd = password_hash( $data->pWd, PASSWORD_BCRYPT );

  $json = json_encode ( $user );
  iWriteFile ( 'Poi3dUsers/' . $data->iD->uNm . '.json', $json );

  return 0;
}

function Set2FA ( $data, &$ret, &$errorText )
{
  $ret = [];
  $errorText = "";

  if ( iLoadUserBaseData( $data, $user, $errorText ) ) {
    return 1;
  }

  $user->twoFA = $data->twoFA;

  if ( !$user->twoFA ) {
    unset( $user->twoFAtoken );
    unset( $user->twoFAtokenTimestamp );
  }

  $json = json_encode ( $user );
  iWriteFile ( 'Poi3dUsers/' . $data->iD->uNm . '.json', $json );

  return 0;
}

function iSentUserTextMail ( $mailAddr, $userName, $text, $language, &$errorText ) {
  $mailConfig = (object) [];
  iLoadMailConfiguration($mailConfig);

  $breaks = array("<br />","<br>","<br/>");
  $text = str_ireplace($breaks, "\r\n", $text);

  if( empty( $mailConfig->user ) || (strpos($mailConfig->user, '@') === false)) {
    $errorText = "wrong sender address defined in configuration";
    return false;
  }

  $mail = new PHPMailer(true);
  try {
    $mail->isSMTP();
    $mail->Host = $mailConfig->host;
    $mail->SMTPAuth = true;
    $mail->Username = $mailConfig->user;
    $mail->Password = $mailConfig->password;
    $mail->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS;
    $mail->Port = $mailConfig->port;

    if ( !empty( $mailConfig->overwrite_recipient ) ) {
      $mail->addAddress( $mailConfig->overwrite_recipient );
    } else {
      $mail->addAddress( $mailAddr );
    }
    $mail->setFrom( $mailConfig->from, $mailConfig->from_name );

    if ( $language == 'de' ) {
      $mail->Subject = 'Mitteilung von Benutzer ' . $userName;
      $mail->Body = $text . "\n\n" . $mailConfig->footer_de;
    }
    else {
      $mail->Subject = 'Message from user ' . $userName;
      $mail->Body = $text . "\n\n" . $mailConfig->footer_en;
    }

    $mail->send();

  } catch( Exception $e ) {
    $errorText = "Couldn't send mail Host=" . $mail->Host . ",Username=" . $mail->Username . ",Port=" . $mail->Port . ",Error=" . $mail->ErrorInfo;
    return false;
  }

  return true;
}

function iSentGroupInvitationMail ( $mailAddr, $wgGroupName, $language, &$errorText ) {
  $mailConfig = (object) [];
  iLoadMailConfiguration($mailConfig);

  $mail = new PHPMailer(true);
  try {
    $mail->isSMTP();
    $mail->Host = $mailConfig->host;
    $mail->SMTPAuth = true;
    $mail->Username = $mailConfig->user;
    $mail->Password = $mailConfig->password;
    $mail->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS;
    $mail->Port = $mailConfig->port;

    if ( !empty( $mailConfig->overwrite_recipient ) ) {
      $mail->addAddress( $mailConfig->overwrite_recipient );
    } else {
      $mail->addAddress( $mailAddr );
    }

    $mail->setFrom( $mailConfig->from_2fa_pwd, $mailConfig->from_2fa_pwd_name );

    $mail->Subject = 'Invitation to work group';
    $mail->Body = "You have been invited to the work group \"" . $wgGroupName . "\"\n\n";
    if ( $language == 'de' ) {
      $mail->Subject = 'Einladung zur Arbeitsgruppe';
      $mail->Body = "Sie wurden zu der Arbeitsgruppe \"" . $wgGroupName . "\" eingeladen\n\n";
    }

    $mail->send();

  } catch( Exception $e ) {
    $errorText = "Couldn't send mail: " . $mail->ErrorInfo;
    return false;
  }

  return true;
}

function iSent2FAMail ( $mailAddr, $token, $language, &$errorText ) {
  $mailConfig = (object) [];
  iLoadMailConfiguration($mailConfig);

  $mail = new PHPMailer(true);
  try {
    $mail->isSMTP();
    $mail->Host = $mailConfig->host;
    $mail->SMTPAuth = true;
    $mail->Username = $mailConfig->user;
    $mail->Password = $mailConfig->password;
    $mail->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS;
    $mail->Port = $mailConfig->port;

    if ( !empty( $mailConfig->overwrite_recipient ) ) {
      $mail->addAddress( $mailConfig->overwrite_recipient );
    } else {
      $mail->addAddress( $mailAddr );
    }

    $mail->setFrom( $mailConfig->from_2fa_pwd, $mailConfig->from_2fa_pwd_name );

    $mail->Subject = 'Poi3d Login';
    $mail->Body = "Please use the following 2FA code to access the Poi3d portal:\n\n" . $token;
    if ( $language == 'de' ) {
      $mail->Subject = 'Poi3d Login';
      $mail->Body = "Bitte verwenden Sie den folgenden 2FA Code um sich im Poi3d-Portal anzumelden:\n\n" . $token;
    }

    $mail->send();

  } catch( Exception $e ) {
    $errorText = "Couldn't send mail: " . $mail->ErrorInfo;
    return false;
  }

  return true;
}

function iSentTmpPwdMail ( $mailAddr, $pwd, $language, &$errorText ) {
  $mailConfig = (object) [];
  iLoadMailConfiguration($mailConfig);

  $mail = new PHPMailer(true);
  try {
    $mail->isSMTP();
    $mail->Host = $mailConfig->host;
    $mail->SMTPAuth = true;
    $mail->Username = $mailConfig->user;
    $mail->Password = $mailConfig->password;
    $mail->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS;
    $mail->Port = $mailConfig->port;

    if ( !empty( $mailConfig->overwrite_recipient ) ) {
      $mail->addAddress( $mailConfig->overwrite_recipient );
    } else {
      $mail->addAddress( $mailAddr );
    }

    $mail->setFrom( $mailConfig->from_2fa_pwd, $mailConfig->from_2fa_pwd_name );

    $mail->Subject = 'Poi3d Password';
    $mail->Body = "You can access the Poi3d portal within the next hour using the temporary password:\n\n" . $pwd;
    if ( $language == 'de' ) {
      $mail->Subject = 'Poi3d Passwort';
      $mail->Body = "Sie koennen sich innerhalb der naechsten Stunde mit dem folgenden temporaeren Passwort anmelden:\n\n" . $pwd;
    }

    $mail->send();

  } catch( Exception $e ) {
    $errorText = "Couldn't send mail: " . $mail->ErrorInfo;
    return false;
  }

  return true;
}

function UploadImage ( $data, &$ret, &$errorText, $files )
{
  $ret = [];
  $errorText = "";

  if ( iLoadUserBaseData( $data, $user, $errorText ) ) {
    return 1;
  }

  if (empty($files) || empty($files['image']) || empty($data->height) || empty($data->fileName)) {
    $errorText = "invalid data structure";
    return 1;
  }

  if ( $files['image']['error'] != UPLOAD_ERR_OK ) {
    $errorText = "error during upload";
    return 1;
  }

  $data->height = intval ( $data->height );

  if ( $data->height <= 0 || $data->height > 3000 ) {
    $errorText = "image height invalid or too big";
    return 1;
  }

  $pathParts = pathinfo ( $data->fileName );

  // just jpg or png
  if ( !preg_match("/^((jpg)|(jpeg)|(png))$/i", $pathParts['extension'] ) ) {
    $errorText = "image has to be png or jpg";
    return 1;
  }

  if ( iCheckPath ( $user, $data->fileName, iGetAccessIds($data), $errorText ) ) {
    return 1;
  }

  // create path if not existing
  if ( !file_exists ( $pathParts['dirname'] ) ) {
    if ( !mkdir( $pathParts['dirname'] ) ) {
      $errorText = "couldn't create path";
      return 1;
    }
  }

  if ( !@move_uploaded_file ( $files['image']['tmp_name'], $data->fileName ) ) {
    $errorText = "couldn't copy file";
    return 1;
  }

  if ( iResizeImage ( $data->fileName, $data->height, $errorText ) ) {
    return 1;
  }

  return 0;
}

function UploadZipFile ( $data, &$ret, &$errorText, $files )
{
  $ret = [];
  $errorText = "";

  if ( iLoadUserBaseData( $data, $user, $errorText ) ) {
    return 1;
  }

  if ( empty ( $files ) || empty( $files['file'] ) || empty ( $data->fileName ) ) {
    $errorText = "invalid data structure";
    return 1;
  }

  if ( $files['file']['error'] != UPLOAD_ERR_OK ) {
    $errorText = "error during upload";
    return 2;
  }


  if ( iCheckPath ( $user, $data->fileName, iGetAccessIds($data), $errorText ) ) {
    return 3;
  }

  $pathParts = pathinfo ( $data->fileName );

  if ( strstr ( strtolower ( $pathParts['extension'] ), 'php' ) || strstr ( strtolower ( $pathParts['extension'] ), 'htaccess' ) ) {
    $errorText = "no.";
    return 4;
  }

  // just zip
  if ( !preg_match("/^((zip))$/i", $pathParts['extension'] ) ) {
    $errorText = "file has to be zip";
    return 5;
  }

  // create path if not existing
  if ( !file_exists ( $pathParts['dirname'] ) ) {
    if ( !mkdir( $pathParts['dirname'] ) ) {
      $errorText = "couldn't create path";
      return 6;
    }
  }

  if ( !@move_uploaded_file ( $files['file']['tmp_name'], $data->fileName ) ) {
    $errorText = "couldn't copy file";
    return 7;
  }

  return 0;
}

function UploadFile ( $data, &$ret, &$errorText, $files )
{
  $ret = [];
  $errorText = "";

  if ( empty ( $files ) || empty( $files['file'] ) || empty ( $data->fileName ) ) {
    $errorText = "invalid data structure";
    return 1;
  }

  if ( iLoadUserBaseData( $data, $user, $errorText ) ) {
    return 1;
  }

  if ( $files['file']['error'] != UPLOAD_ERR_OK ) {
    $errorText = "error during upload";
    return 2;
  }


  if ( iCheckPath ( $user, $data->fileName, iGetAccessIds($data), $errorText ) ) {
    return 3;
  }
  $pathParts = pathinfo ( $data->fileName );

  // create path if not existing
  if ( !file_exists ( $pathParts['dirname'] ) ) {
    if ( !mkdir( $pathParts['dirname'] ) ) {
      $errorText = "couldn't create path";
      return 6;
    }
  }

  if ( !@move_uploaded_file ( $files['file']['tmp_name'], $data->fileName ) ) {
    $errorText = "couldn't copy file";
    return 7;
  }

  return 0;
}

function CheckFileExistance ( $data, &$ret, &$errorText )
{
  $ret = [];
  $errorText = "";

  if ( empty ( $data->fileName ) ) {
    $errorText = "no file name given";
    return 1;
  }

  if ( file_exists ( $data->fileName ) ) {
    $ret = json_decode ("{\"ex\":true}");
  }
  else {
    $ret = json_decode ("{\"ex\":false}");
  }

  return 0;
}

function PushMessage ( $data, &$ret, &$errorText )
{
  $ret = [];
  $errorText = "";

  if ( iLoadUserBaseData( $data, $user, $errorText ) ) {
    return 1;
  }

  if ( empty ( $data->recipients ) ) {
    $errorText = "no recipients given";
    return 2;
  }

  if ( empty ( $data->tXt ) ) {
    $errorText = "no message text given";
    return 3;
  }

  if ( empty ( $data->aCt ) ) {
    $errorText = "no action type";
    return 4;
  }

  if ( empty ( $data->oDt ) ) {
    $errorText = "no object data given";
    return 6;
  }

  if ( !is_array ( $data->recipients ) ) {
    $errorText = "invalid data structure (recipients)";
    return 7;
  }

  return (iPushMessageToUsers ($user->aL, $data->recipients, $data->aCt, $data->dId, "", $data->tXt, $data->oDt, $errorText));
}

function PushSubscribeOrUpdate ( $data, &$ret, &$errorText )
{
  $ret = (object) [];
  $errorText = "";

  if ( iLoadUserBaseData( $data, $user, $errorText ) ) {
    return 1;
  }

  $username = $data->iD->uNm;

  if ( empty( $data->endpoint ) || empty( $data->key ) || empty( $data->auth ) || empty ( $data->contentEncoding ) ) {
    $errorText = "missing push data";
    return 2;
  }

  $filename = 'SignedUsers/' . $username . '.json';

  if(file_exists( $filename )) {
  $f = iReadFile ( $filename );
    $fContent = json_decode ( $f );
  }
  else {
    $fContent = (object) [];
    $fContent->callOffer = (object) [];
    $fContent->callAnswer = (object) [];
    $fContent->endpoints[] = [];
  }

  $endpoints = [];

  $endpoint = [
    'endpoint' => $data->endpoint,
    'key' => $data->key,
    'auth' => $data->auth,
    'contentEncoding' => $data->contentEncoding
  ];

  $updated = false;

  for ( $i = 0; $i < count( $endpoints ); $i++ ) {
    if ( $endpoints[$i]->endpoint == $data->endpoint ) {
      $endpoints[$i] = $endpoint;
      $updated = true;
    }
  }

  if ( !$updated ) {
    $endpoints[] = $endpoint;
  }

  $fContent->endpoints = $endpoints;

  $fContent_json = json_encode( $fContent );

  if ( iWriteFile( $filename, $fContent_json ) === false ) {
    $errorText = "couldn't save endpoint";
    return 3;
  }

  return 0;
}

function PushUnsubscribe ( $data, &$ret, &$errorText ) {
  $ret = [];
  $errorText = "";

  if ( iLoadUserBaseData( $data, $user, $errorText ) ) {
    return 1;
  }

  $username = $data->iD->uNm;

  if ( empty( $data->endpoint ) ) {
    $errorText = "missing endpoint";
    return 2;
  }

  $filename = 'SignedUsers/' . $username . '.json';

  if(file_exists( $filename )) {
  $f = iReadFile ( $filename );
    $fContent = json_decode ( $f );
  }
  else {
    return 0;
  }

  if ( empty ( $fContent->endpoints ) ) {
    return 0;
  }

  // keep all endpoints unequal to the requested one
  $endpoints_new = [];
  foreach ( $fContent->endpoints as $endpoint ) {
    if ( $endpoint->endpoint != $data->endpoint ) {
      $endpoints_new[] = $endpoint;
    }
  }
  $fContent->endpoints = $endpoints_new;
  $fContent_json = json_encode( $fContent );

  if ( iWriteFile( $filename, $fContent_json  ) === false ) {
    $errorText = "couldn't save updated endpoints";
    return 3;
  }

  return 0;
}

function SendEmail ( $data, &$ret, &$errorText )
{
  $ret = [];
  $errorText = "";

  if ( iLoadUserBaseData( $data, $user, $errorText ) ) {
    return 1;
  }

  if ( empty ( $data->resipNames ) ) {
    $errorText = "no recipients given";
    return 2;
  }

  if ( empty ( $data->text ) ) {
    $errorText = "no mail text given";
    return 3;
  }

  $recipients = json_decode ( $data->resipNames );
  if ( !is_array ( $recipients ) ) {
    $errorText = "invalid data structure (resipNames)";
    return 4;
  }

  $mailConfig = (object) [];
  iLoadMailConfiguration($mailConfig);

  if( empty( $mailConfig->user ) || (strpos($mailConfig->user, '@') === false)) {
    $errorText = "wrong sender address defined in configuration";
    return 5;
  }

  $allSent = 1;
  foreach ( $recipients as $u ) {
    //Send email
    $uiLang="en";
    $eml="";
    if ( iGetUserLanguageAndEmail($u->aL, $u->uId, $uiLang, $eml, $errorText) == 0 ) {
      iSentUserTextMail($eml, $user->aL, $data->text, $uiLang, $errorText);
    }
    else
      $allSent = -1;
  }

  if($allSent == 1)
    return 0;
  else {
    $errorText = "not all messages were sent";
    return 6;
  }
}

//CRUD User ------
function CreateUser ($data, &$ret, &$errorText)
{
  $ret = (object) [];
  $errorText = "";

  if ( empty ( $data->uNm ) || empty ( $data->pWd ) || empty ( $data->emL ) || empty ( $data->uId ) ) {
    $errorText = "invalid data structure";
    return 1;
  }

  //create initial admin user
  if($data->cfA == true) {
    $srcDir = 'Poi3dUsers';
    $d = opendir( $srcDir );

    if ( !$d )
    {
      $errorText = "could not create initial admin user, Poi3dUsers directory not found";
      return 2;
    }

    while ( ($f = readdir( $d )) !== false ) {
      if ( $f != '.' && $f != '..' ) {
        $p = $srcDir . '/' . $f;

        if ((strpos($p, '.json') !== false)) {
          $errorText = "could not create initial admin user, Poi3dUsers directory not empty";
          closedir ( $d );
          return 2;
        }
      }
    }
    closedir ( $d );
  }//if($data->cfA == true)

  // prevent special characters
  if ( !preg_match("`^[@.\w-]+$`", $data->uNm ) ) {
    $errorText = "illegal characters in username";
    return 2;
  }

  //prevent mass registration (server only)
  if(iIsPoi3dEnv() == true) {
    $ip = $_SERVER['REMOTE_ADDR'];
    if(iCheckIp ( $ip, $error ) == 0){
      $errorText = "two many registrations from this ip address, please try again in 10 minutes";
      return 3;
    }
  }

  if(iIsPoi3dEnv() == false) {
    $files = scandir('Poi3dUsers/');
    $num_files = count($files)-4; //.,..,.htaccess,UserList.json
    if ( $num_files >= $data->maxU ) {
      $errorText = "too many users registered";
      return 4;
    }

    //only admin can create users in hosted portals
    if($data->cfA == false) {
      if (! iLoadUserBaseData( $data, $user, $errorText ) ) {
        if( $user->tyP != 's' ) {
          $errorText = "no admin rights";
          return 5;
        }
      }
      else {
        $errorText = "no admin rights";
        return 5;
      }
    }
  }

  if ( iFileExistNocase ( 'Poi3dUsers/' . $data->uNm . '.json' ) ) {
    $errorText = "user already exists";
    return 5;
  }

  //create user profile directory
  $srcDir = "Templates/user";
  $tgtDir = iGetCurrentHomeDirectory() . $data->uId;
  if ( !mkdir ( $tgtDir ) ) {
    $errorText = "couldn't create user target dir";
    return 6;
  }

  if ( !ixCopy ( $srcDir, $tgtDir ) ) {
    $errorText = "couldn't copy dir";
    return 7;
  }

  //create user content directory
  $srcDir = "Templates/content";
  $tgtDir = iGetCurrentContentDirectory() . $data->uId;
  if ( !mkdir ( $tgtDir ) ) {
    $errorText = "couldn't create content target dir";
    return 8;
  }

  if ( !ixCopy ( $srcDir, $tgtDir ) ) {
    $errorText = "couldn't copy dir";
    return 9;
  }

  //create user
  $newUser = (object) [];

  $newUser->uId = $data->uId;
  $newUser->aL = $data->uNm;
  $newUser->tlNr = "";

  if(iIsPoi3dEnv() == true)
    $newUser->tyP = "c";
  else if($data->cfA == true)
    $newUser->tyP = "s";
  else
    $newUser->tyP = $data->uType;

  $newUser->emL = $data->emL;
  $newUser->pWd = password_hash( $data->pWd, PASSWORD_BCRYPT );
  $newUser->hmDir = iGetCurrentHomeDirectory () . $newUser->uId;
  $newUser->docDir = iGetCurrentContentDirectory() . $newUser->uId;

  $json = json_encode ( $newUser );
  if ( !iWriteFile ( 'Poi3dUsers/' . $data->uNm . '.json', $json ) ) {
    $errorText = "Could not write user file.";
    return 10;
  }

  iCreateJsonUserList();

  //check profile.json
  $filename = $newUser->hmDir . '/profile.json';
  $f = iReadFile ( $filename );

  //poi3d server only
  if(iIsPoi3dEnv() == true) {

    //email
    try {
      $mail = new PHPMailer(true);
      $mailConfig = (object) [];
      iLoadMailConfiguration($mailConfig);

      $mail->isSMTP();
      $mail->Host = $mailConfig->host;
      $mail->SMTPAuth = true;
      $mail->Username = $mailConfig->user;
      $mail->Password = $mailConfig->password;
      $mail->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS;
      $mail->Port = $mailConfig->port;
      $mail->addAddress( 'info@cadmai.com' );
      $mail->setFrom( $mailConfig->from, $mailConfig->from_name );
      $mail->Subject = 'new user ' . $data->uNm . '/' . $data->emL . '/' . $data->uId . ' in poi3d created';
      $mail->Body = 'New User (Name=' . $data->uNm . ' Email=' . $data->emL . ' Id=' . $data->uId . ') in poi3d created';

      $mail->send();

    } catch( Exception $e ) {}

    //update profile.json
    if ( !$f ) {
      $errorText = "User successfully created, couldn't add author rights.";
      return 10;
    }

    $uDetails = json_decode ( $f );

    //set to 3 documents and 1 work group
    $uDetails->tyPS = $newUser->tyP;
    $uDetails->nDoc = 3;
    $uDetails->nDcS = 3;
    $uDetails->nWg = 1;
    $uDetails->nWgS = 1;
    $uDetails->nFatt = 3;
    $uDetails->nFaS = 3;

    $json = json_encode ( $uDetails );
    iWriteFile ( $filename, $json ) ;
  }

  $ret->uId = $newUser->uId;
  $ret->aL = $newUser->aL;
  $ret->emL = $newUser->emL;
  $ret->tyP = $newUser->tyP;

  return 0;
}

function ReadUserAccessDataByAdmin ( $data, &$ret, &$errorText )
{
  $ret = (object) [];
  $errorText = "";

  if ( iLoadUserBaseData( $data, $user, $errorText ) ) {
    return 1;
  }

  //check admin is calling
  if (empty ( $data->iD->aNm ) ){
    $errorText = "invalid admin";
    return 2;
  }

  if ( empty ( $user->hmDir ) ) {
    $errorText = "hmDir of user not set";
    return 3;
  }

  $userDirName = $user->hmDir;

  if ( iCheckPath ( $user, $userDirName, iGetAccessIds($data), $errorText ) ) {
    return 4;
  }

  $userProfileFname  = $userDirName . '/profile.json';

  $ret->tyP = $user->tyP;
  $ret->lck = false;
  if(!empty ( $user->lck )) $ret->lck = $user->lck;

  $ret->nDoc = 0;
  $ret->nWg = 0;
  $ret->nFatt = 0;
  $ret->aExpDt = "";

  if(file_exists($userProfileFname)) {
    $p = iReadFile ( $userProfileFname );
    $prof = json_decode ( $p );

    if(empty($prof->tyPS)) {$ret->tyPS = $user->tyP;} else {$ret->tyPS = $prof->tyPS;}
    if(empty($prof->nDcS)) {$ret->nDcS = $prof->nDoc;} else {$ret->nDcS = $prof->nDcS;}
    if(empty($prof->nWgS)) {$ret->nWgS = $prof->nWg;} else {$ret->nWgS = $prof->nWgS;}
    if(empty($prof->nFaS)) {$ret->nFaS = $prof->nFatt;} else {$ret->nFaS = $prof->nFaS;}

    $ret->nDoc = $prof->nDoc;
    $ret->nWg = $prof->nWg;
    $ret->nFatt = $prof->nFatt;
    $ret->aExpDt = "2000-01-01T00:00:00.000Z";
    if(!empty ( $prof->aExpDt )) $ret->aExpDt = $prof->aExpDt;
  }

  return 0;
}

function ReadUserBaseData ( $data, &$ret, &$errorText )
{
  $ret = (object)[];
  $errorText = "";

  if ( iLoadUserBaseData( $data, $ret, $errorText ) ) {
    return 1;
  }

  if ( isset( $ret->twoFA ) && $ret->twoFA ) {
    // 2FA login

    // 1. no token sent yet
    // 2. token too old (prevents also spamming)
    if ( empty( $ret->twoFAtoken ) || empty( $ret->twoFAtokenTimestamp ) || (time() - intval( $ret->twoFAtokenTimestamp ) > 60*60*6) ) {
      // sent 2FA
      $bytes = random_bytes(10);
      $token = bin2hex($bytes);

      $ret->twoFAtoken = $token;
      $ret->twoFAtokenTimestamp = time();

      $json = json_encode ( $ret );
      iWriteFile ( 'Poi3dUsers/' . $data->iD->uNm . '.json', $json );

      $uiLang="en";
      iGetUserLanguage($ret->uId, $uiLang, $errorText);

      if ( !iSent2FAMail( $ret->emL, $token, $uiLang, $errorText ) ) {
        $ret = (object)[];
        return 4;
      }

      $errorText = "New 2FA token sent.";
      $ret = (object)[];
      return 2;
    }

    if ( $data->iD->token != $ret->twoFAtoken ) {
      $ret = (object)[];
      $errorText = "Token invalid";
      return 3;
    }

  }

  //remove password hash
  $ret->pWd = "";
  $ret->tmpPwd = "";
  $ret->twoFAtoken = "";

  // write login-info
  // create path if not existing
  if ( !file_exists ( 'SignedUsers' ) ) {
    if ( !mkdir( 'SignedUsers' ) ) {
      return;
    }
  }

  return 0;
}

function LogOff ( $data, &$ret, &$errorText )
{
  $ret = [];
  $errorText = "";

  if ( iLoadUserBaseData( $data, $user, $errorText ) ) {
    return 1;
  }
  // delete login-info
  $loginFlNm = 'SignedUsers/' . $data->iD->uNm . '.json';
  unlink ( $loginFlNm );
}

function CheckUserLogins ( $data, &$ret, &$errorText)
{
  $ret = [];
  $errorText = "";

  if ( iLoadUserBaseData( $data, $user, $errorText ) ) {
    return 1;
  }

  if ( empty ( $data->userNames ) ) {
    $errorText = "no user names given";
    return 2;
  }

  $userNames = json_decode ( $data->userNames );
  if ( !is_array ( $userNames ) ) {
    $errorText = "invalid data structure (userNames)";
    return 3;
  }

  foreach ( $userNames as $u ) {
    $loginInfo = (object) [];
    $loginInfo->aL = $u->aL;

    $loginFlNm = 'SignedUsers/' . $u->aL. '.json';
    if (file_exists($loginFlNm)) {
      $fileDate = new DateTime(date(DATE_ATOM, filemtime($loginFlNm)));
      $currDate = new DateTime();
      $interval = date_diff($fileDate, $currDate);

      if($interval->d > 0)
        $loginInfo->lI = 0;
      else
        $loginInfo->lI = 1;
    }
    else
      $loginInfo->lI = 0;

    $ret[] = $loginInfo;
  }

  return 0;
}

function ResetUserPassword ( $data, &$ret, &$errorText )
{
  $ret = (object)[];
  $errorText = "";

  if ( empty( $data->iD->uNm ) ) {
    $errorText = "Invalid username";
    return 1;
  }

  $username = $data->iD->uNm;

  if ( iGetUser( $username, $user, $errorText ) ) {
    $errorText = "Invalid username";
    return 2;
  }

  if ( !empty( $user->tmpPwdTimestamp ) && (time() - intval( $user->tmpPwdTimestamp ) < 60*60*1) ) {
    $errorText = "Too frequent usage.";
    return 3;
  }

  $bytes = random_bytes(10);
  $token = bin2hex($bytes);

  $user->tmpPwd = password_hash( $token, PASSWORD_BCRYPT );
  $user->tmpPwdTimestamp = time();

  $json = json_encode ( $user );
  iWriteFile ( 'Poi3dUsers/' . $data->iD->uNm . '.json', $json );

  $uiLang="en";
  iGetUserLanguage($user->uId, $uiLang, $errorText);

  if ( !iSentTmpPwdMail( $user->emL, $token, $uiLang, $errorText ) ) {
    return 3;
  }

  $errorText = "Mail sent.";

  return 0;
}

function ReadUser ($data, &$ret, &$errorText)
{
  $ret = (object) [];
  $errorText = "";

  if ( iLoadUserBaseData( $data, $user, $errorText ) ) {
    return 1;
  }

  if ( empty ( $user->hmDir ) ) {
    $errorText = "hmDir of user not set";
    return 1;
  }

  $userDirName = $user->hmDir;

  if ( iCheckPath ( $user, $userDirName, iGetAccessIds($data), $errorText ) ) {
    return 1;
  }

  $userProfileFname  = $userDirName . '/profile.json';
  $userDocsFname = $userDirName . '/documents.json';
  $userWgFname = $userDirName . '/workGroups.json';

  $updateProfile = false;
  $prof = (object) [];
  if(file_exists($userProfileFname)) {
    $p = iReadFile ( $userProfileFname );
    $prof = json_decode ( $p );

    //update
    if (!property_exists($prof, 'tyPS')) {$prof->tyPS = $user->tyP; $updateProfile = true;}
    if (!property_exists($prof, 'nDoc')) {$prof->nDoc = 3; $updateProfile = true;}
    if (!property_exists($prof, 'nDcS')) {$prof->nDcS = $prof->nDoc; $updateProfile = true;}
    if (!property_exists($prof, 'nWg')) {$prof->nWg = 1; $updateProfile = true;}
    if (!property_exists($prof, 'nWgS')) {$prof->nWgS = $prof->nWg; $updateProfile = true;}
    if (!property_exists($prof, 'nFatt')) {$prof->nFatt = 3; $updateProfile = true;}
    if (!property_exists($prof, 'nFaS')) {$prof->nFaS = $prof->nFatt; $updateProfile = true;}

    if($updateProfile == true) {
      $json = json_encode ( $prof );
      iWriteFile ( $userProfileFname, $json ) ;
    }

  }
  else {
    $errorText = "couldn't open " . $userProfileFname;
    return 1;
  }

  $docs = [];
  if(file_exists($userDocsFname)) {
    $d = iReadFile ( $userDocsFname );
    $docs = json_decode ( $d );
  }

  $groups = [];
  if(file_exists($userWgFname)) {
    $g = iReadFile ( $userWgFname );
    $groups = json_decode ( $g );
  }

  $ret->prof = $prof;
  $ret->docs = $docs;
  $ret->groups = $groups;

  return 0;
}

function UpdateUserBaseData ( $data, &$ret, &$errorText )
{
  $ret = [];
  $errorText = "";
  $userProfileFname  = 'Poi3dUsers/' . $data->iD->uNm . '.json';
  $changeableKeys = [ 'tlNr', 'emL'];

  if ( iLoadUserBaseData( $data, $user, $errorText ) ) {
    return 1;
  }

  foreach ( $data as $k => $v ) {
    if ( in_array( $k, $changeableKeys ) ) {
      $user->$k = $v;
    } elseif ( $k != 'iD' ) {
      $errorText = 'not all fields updated';
    }
  }

  $json = json_encode ( $user );
  iWriteFile ( $userProfileFname, $json );

  return 0;
}

function UpdateUser ($data, &$ret, &$errorText)
{
  $ret = [];
  $errorText = "";
  $userProfileFname  = 'Poi3dUserData/' . $data->iD->uId . '/profile.json';
  $changeableKeys = [ 'fN', 'lN', 'pT', 'dStdSel', 'dVrSel', 'explF', 'fAsTp', 'brPos', 'sLB', 'sRB', 'uiLng', 'uiCol', 'dltPos', 'bomUrl' ];

  if ( iLoadUserBaseData( $data, $user, $errorText )) {
    return 1;
  }

  $p = iReadFile ( $userProfileFname );
  $prof = json_decode ( $p );

  foreach ( $data as $k => $v ) {
    if ( in_array( $k, $changeableKeys ) ) {
      $prof->$k = $v;
    } elseif ( $k != 'iD' ) {
      $errorText = 'not all fields updated';
    }
  }

  $json = json_encode ( $prof );
  iWriteFile ( $userProfileFname, $json );

  return 0;
}

function DeleteUser( $data, &$ret, &$errorText)
{
  $ret = [];
  $user = [];
  $errorText = "";
  $errState = 0;

  if ( iLoadUserBaseData( $data, $user, $errorText ) ) {
    return 1;
  }

  //1. delete work groups
  $wgListFileName =$user->hmDir . "/workGroups.json";
  $wgList = [];

  if(file_exists( $wgListFileName )) {
    $f = iReadFile ( $wgListFileName );
    $wgList = json_decode ( $f );
  }

  foreach ( $wgList as $wg ) {
    if($wg->owner == $user->uId) { //delete own groups
      iRemoveWorkGroup($user, $wg->grId, "", "deleteUser", $errorText);
    }
    else { //remove user from other groups
      $wgUsersFileName = './Poi3dUserContent/' . $wg->owner . '/WorkGroups/' . $wg->grId . '/users.json';
      iRemoveUserWgReference($wgUsersFileName,$user->uId);
    }
  }

  //2. delete documents
  $docListFileName =$user->hmDir . "/documents.json";
  $docList = [];

  if(file_exists( $docListFileName )) {
    $f = iReadFile ( $docListFileName );
    $docList = json_decode ( $f );
  }

  foreach ( $docList as $doc ) {
    iRemoveDocument($user, $doc->dId, "deleteUser", $errorText);
  }

  //3. delete files and folders
  //delete document directory
  if (! empty ( $user->docDir )&& file_exists( $user->docDir )) {
    if ( iCheckPath ( $user, $user->docDir, iGetAccessIds($data), $errorText ) == 0 ) {
      if ( !iRmDir ( $user->docDir ) ) {
        $errorText = "couldn't delete doc directory";
        $errState = 1;
      }
    }
  }

  //delete home directory
  if (! empty ( $user->hmDir ) && file_exists( $user->hmDir )) {
    if ( iCheckPath ( $user, $user->hmDir, iGetAccessIds($data), $errorText ) == 0 ) {
      if ( !iRmDir ( $user->hmDir ) ) {
        $errorText = "couldn't delete home directory";
        $errState = 2;
      }
    }
  }

  //delete user file
  $userFile = 'Poi3dUsers/' . $user->aL . '.json';
  if ( !unlink ( $userFile ) ) {
    $errorText = "couldn't delete user file " . $userFile;
    $errState = 3;
    return 1;
  }

  //delete SignedIn information
  $userFile = 'SignedUsers/' . $user->aL . '.json';
  if(file_exists( $userFile )) {
    unlink ( $userFile );
  }

  //update user list
  if (! iCreateJsonUserList() ) {
    $errorText = "couldn't update user list";
    $errState = 4;
  }

  //email (server only)
  if(iIsPoi3dEnv() == true) {

    try {
      $mail = new PHPMailer(true);
      $mailConfig = (object) [];
      iLoadMailConfiguration($mailConfig);

      $mail->isSMTP();
      $mail->Host = $mailConfig->host;
      $mail->SMTPAuth = true;
      $mail->Username = $mailConfig->user;
      $mail->Password = $mailConfig->password;
      $mail->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS;
      $mail->Port = $mailConfig->port;
      $mail->addAddress( 'info@cadmai.com' );
      $mail->setFrom( $mailConfig->from, $mailConfig->from_name );
      $mail->Subject = 'user ' . $user->aL . ' deleted';
      $mail->Body = 'user (Name=' . $user->aL . ', Id=' . $user->uId . ') deleted';

      $mail->send();

    } catch( Exception $e ) {
      $errorText = "Couldn't send mail Host=" . $mail->Host . ",Username=" . $mail->Username . ",Port=" . $mail->Port . ",Error=" . $mail->ErrorInfo;
      return 0;
    }
  }

  return 0;
}

//CRUD Document ------
function CreateDocument ( $data, &$ret, &$errorText, $files )
{
  $ret = [];
  $errorText = "";

  if ( iLoadUserBaseData( $data, $user, $errorText ) ) {
    return 1;
  }

  if ( empty ( $files ) || empty( $files['file'] ) || empty ( $data->fileName ) ) {
    $errorText = "invalid data structure";
    return 1;
  }

  if ( $files['file']['error'] != UPLOAD_ERR_OK ) {
    $errorText = "error during upload";
    return 2;
  }

  if ( iCheckPath ( $user, $data->fileName, iGetAccessIds($data), $errorText ) ) {
    return 3;
  }

  $pathParts = pathinfo ( $data->fileName );

  // todo: better explicitly define which file types are allowed
  if ( strstr ( strtolower ( $pathParts['extension'] ), 'php' ) || strstr ( strtolower ( $pathParts['extension'] ), 'htaccess' ) ) {
    $errorText = "no.";
    return 4;
  }

  // create path if not existing
  if ( !file_exists ( $pathParts['dirname'] ) ) {
    if ( !mkdir( $pathParts['dirname'] ) ) {
      $errorText = "couldn't create path";
      return 5;
    }
  }

  if ( !@move_uploaded_file ( $files['file']['tmp_name'], $data->fileName ) ) {
    $errorText = "couldn't copy file";
    return 6;
  }

  //create metadata
  if(!empty ( $data->prof ))
  {
    $docProfileFname  = $pathParts['dirname'] . '/profile.json';
    $json = json_encode ( $data->prof );
    //echo ("docProfileFname " . $docProfileFname). "\n";
    //echo ("json " . $json). "\n";
    iWriteFile ( $docProfileFname, $json );
  }

  return 0;
}

function ReadDocument ($data, &$ret, &$errorText)
{
  $ret = (object) [];
  $errorText = "";
  $globalAccess = false;

  if ( empty ( $data->dirName ) ) {
    $errorText = "dirName not set";
    return 1;
  }

  $docProfileFname = $data->dirName . '/profile.json';
  $docWgFname = $data->dirName . '/workGroups.json';
  $globalAccess = iCheckGlobalAccess($docProfileFname);

  if ($globalAccess == false){
    if(empty ( $data->iD->uNm )) {
      $errorText = "document access not possible";
      return 2;
    }

    if ( iLoadUserBaseData( $data, $user, $errorText ) ) {
      return 2;
    }
  }

  $prof = [];
  $content = iGetJsonFromFile($docProfileFname,true);
  $prof = $content->data;

  $groups = [];
  if(file_exists($docWgFname)) {
    $g = iReadFile ( $docWgFname );
    $groups = json_decode ( $g );
  }

  $ret->prof = $prof;
  $ret->groups = $groups;

  return 0;
}

function LoadDocumentAttachments ( $data, &$ret, &$errorText, &$srvInfo)
{
  $ret = (object) [];
  $srvInfo = (object)[];
  $errorText = "";

  if (iCheckGlobalAccess($data->dirName) == false) {
    if ( iLoadUserBaseData( $data, $user, $errorText ) ) {
      return 1;
    }

    if ( empty ( $data->dirName ) ) {
      $errorText = "dirName not set";
      return 2;
    }
  }

  $docDirName = $data->dirName;
  $docAnnFname = $docDirName . '/annotations.json';
  $docLocFname = $docDirName . '/locations.json';
  $docInstrFname = $docDirName . '/instructions.json';
  $docMatSetsFname = $docDirName . '/materialSets.json';
  $docFileAttsFname = $docDirName . '/fileAttachments.json';
  $docBomFname = $docDirName . '/boms.json';
  $docChatFname = $docDirName . '/docChat.json';
  $docLogFname = $docDirName . '/fileAccess.json';
  $docFattLogFname = $docDirName . '/Files/fileAccess.json';
  $docSensorsFname = $docDirName . '/sensors.json';
  $docAttrFname = $docDirName . '/docAttributes.json';

  $anns = [];
  $content = iGetJsonFromFile($docAnnFname,true);
  if(($content->isRemote == false)&&(!empty($content->data))) {
    $srvInfo->annModDate = date(DATE_ATOM, filemtime($docAnnFname));
  }
  else {
    $srvInfo->annModDate = date(DATE_ATOM, time());
  }
  $anns = $content->data;

  $locs = [];
  $content = iGetJsonFromFile($docLocFname,true);
  if(($content->isRemote == false)&&(!empty($content->data))) {
    $srvInfo->locaModDate = date(DATE_ATOM, filemtime($docLocFname));
  }
  else {
    $srvInfo->locaModDate = date(DATE_ATOM, time());
  }
  $locs = $content->data;

  $insts = [];
  $content = iGetJsonFromFile($docInstrFname,true);
  if(($content->isRemote == false)&&(!empty($content->data))) {
    $srvInfo->instModDate = date(DATE_ATOM, filemtime($docInstrFname));
  }
  else {
    $srvInfo->instModDate = date(DATE_ATOM, time());
  }
  $insts = $content->data;

  $sensors = [];
  $content = iGetJsonFromFile($docSensorsFname,true);
  if(($content->isRemote == false)&&(!empty($content->data))) {
    $srvInfo->snsModDate = date(DATE_ATOM, filemtime($docSensorsFname));
  }
  else {
    $srvInfo->snsModDate = date(DATE_ATOM, time());
  }
  $sensors = $content->data;

  $msets = [];
  $content = iGetJsonFromFile($docMatSetsFname,true);
  if(($content->isRemote == false)&&(!empty($content->data))) {
    $srvInfo->matModDate = date(DATE_ATOM, filemtime($docMatSetsFname));
  }
  else {
    $srvInfo->matModDate = date(DATE_ATOM, time());
  }
  $msets = $content->data;

  $fatts = [];
  $content = iGetJsonFromFile($docFileAttsFname,true);
  if(($content->isRemote == false)&&(!empty($content->data))) {
    $srvInfo->fattModDate = date(DATE_ATOM, filemtime($docFileAttsFname));
  }
  else {
    $srvInfo->fattModDate = date(DATE_ATOM, time());
  }
  $fatts = $content->data;

  $boms = [];
  $content = iGetJsonFromFile($docBomFname,true);
  if(($content->isRemote == false)&&(!empty($content->data))) {
    $srvInfo->bomModDate = date(DATE_ATOM, filemtime($docBomFname));
  }
  else {
    $srvInfo->bomModDate = date(DATE_ATOM, time());
  }
  $boms = $content->data;

  $chat = [];
  $content = iGetJsonFromFile($docChatFname,true);
  if(($content->isRemote == false)&&(!empty($content->data))) {
    $srvInfo->chatModDate = date(DATE_ATOM, filemtime($docChatFname));
  }
  else {
    $srvInfo->chatModDate = date(DATE_ATOM, time());
  }
  $chat = $content->data;

  $docLog = [];
  $content = iGetJsonFromFile($docLogFname,true);
  if(($content->isRemote == false)&&(!empty($content->data))) {
    $srvInfo->docLogModDate = date(DATE_ATOM, filemtime($docLogFname));
  }
  else {
    $srvInfo->docLogModDate = date(DATE_ATOM, time());
  }
  $docLog = $content->data;

  $fattLog = [];
  $content = iGetJsonFromFile($docFattLogFname,true);
  if(($content->isRemote == false)&&(!empty($content->data))) {
    $srvInfo->fattLogModDate = date(DATE_ATOM, filemtime($docFattLogFname));
  }
  else {
    $srvInfo->fattLogModDate = date(DATE_ATOM, time());
  }
  $fattLog = $content->data;

  $docAttr = [];
  $content = iGetJsonFromFile($docAttrFname,true);
  if(($content->isRemote == false)&&(!empty($content->data))) {
    $srvInfo->docAttrModDate = date(DATE_ATOM, filemtime($docAttrFname));
  }
  else {
    $srvInfo->docAttrModDate = date(DATE_ATOM, time());
  }
  $docAttr = $content->data;

  $ret->anns = $anns;
  $ret->locs = $locs;
  $ret->insts = $insts;
  $ret->sensors = $sensors;
  $ret->msets = $msets;
  $ret->fatts = $fatts;
  $ret->boms = $boms;
  $ret->chat = $chat;
  $ret->docLog = $docLog;
  $ret->fattLog = $fattLog;
  $ret->docAttr = $docAttr;

  return 0;
}

function UpdateDocument ($data, &$ret, &$errorText)
{
  $ret = [];
  $errorText = "";

  if ( iLoadUserBaseData( $data, $user, $errorText ) ) {
    return 1;
  }

  if ( empty ( $data->dirName ) ) {
    $errorText = "dirName not set";
    return 3;
  }

  $docProfileFname = $data->dirName . '/profile.json';

  $prof = (object) [];
  if(file_exists($docProfileFname)) {
    $p = iReadFile ( $docProfileFname );
    $prof = json_decode ( $p );
  }

  $changeableKeys = [ 'dTyp', 'owner', 'ttL', 'flNm', 'preFlNm', 'desc', 'uFaT', 'pub', 'exDt'];
  $mDt = $data->prof;

  //echo ("docProfileFname " . $docProfileFname). "\n";
  //echo ("mDt " . $mDt). "\n";

  foreach ( $mDt as $k => $v ) {
    if ( in_array( $k, $changeableKeys ) ) {
      $prof->$k = $v;
    }
  }

  $json = json_encode ( $prof );
  iWriteFile ( $docProfileFname, $json );

  return 0;
}

function DeleteDocument ($data, &$ret, &$errorText)
{
  $ret = [];
  $errorText = "";

  if ( iLoadUserBaseData( $data, $user, $errorText ) ) {
    return 1;
  }

  $err = iRemoveDocument($user, $data->docId, "delete", $errorText);

  if( $err != 0){
    return $err+10;
  }

  //notify group users
  if (!empty ( $data->recipients )  && is_array ( $data->recipients ) ) {
      $ntfObjData = (object) [];
    iPushMessageToUsers ($user->aL, $data->recipients, "docDeleted", $data->docId, "", "Document " . $data->docId . " deleted", $ntfObjData, $errorText);
  }

  return 0;
}

//CRUD WorkGroup ------
function CreateWorkGroup ($data, &$ret, &$errorText)
{
  $ret = [];
  $errorText = "";

  if ( iLoadUserBaseData( $data, $user, $errorText ) ) {
    return 1;
  }

  $grpDirName = "";

  if(empty($data->gDir)) {
    if ( empty ( $user->docDir ) ) {
      $errorText = "docDir of user not set";
      return 2;
    }
    $grpDirName = $user->docDir . '/WorkGroups/' . $data->gId;
  }
  else
    $grpDirName = $data->gDir;

  $userList = json_decode ( $data->users );
  if ( !is_array ( $userList ) ) {
    $errorText = "invalid data structure (users)";
    return 3;
  }

  $userToAddList = json_decode ( $data->usersToAdd );
  if ( !is_array ( $userToAddList ) ) {
    $errorText = "invalid data structure (usersToAdd)";
    return 4;
  }

  $docList = json_decode ( $data->docs );
  if ( !is_array ( $docList ) ) {
    $errorText = "invalid data structure (docs)";
    return 6;
  }

  $docsToAddList = json_decode ( $data->docsToAdd );
  if ( !is_array ( $docsToAddList ) ) {
    $errorText = "invalid data structure (docsToAdd)";
    return 7;
  }

  // create path if not existing
  if ( !file_exists ( $grpDirName ) ) {
    if ( !mkdir( $grpDirName ) ) {
      $errorText = "couldn't create path";
      return 9;
    }
  }

  $grpDocsFname = $grpDirName . '/documents.json';
  $grpUsersFname = $grpDirName . '/users.json';
  $grpProfileFile = $grpDirName . '/profile.json';

  //write meta data
  if ( !iWriteFile( $grpProfileFile, $data->prof ) ) {
    $errorText = "couldn't write" . $grpProfileFile;
    return 10;
  }

  // create doc list file
  if ( !iWriteFile( $grpDocsFname, $data->docs ) ) {
    $errorText = "couldn't write" . $grpDocsFname;
    return 11;
  }

  // create user list file
  if ( !iWriteFile( $grpUsersFname, $data->users ) ) {
    $errorText = "couldn't write" . $grpUsersFname;
    return 12;
  }

  $grpProf = json_decode ( $data->prof );

  if(strpos($grpDirName, 'Poi3dCatalog') !== false) {
    //add workgroup entry to catalog
    $catEntriesFname = 'Poi3dCatalog/catalog.json';
    iAddWorkGroupReference($catEntriesFname,$data->gId,$grpProf->grNm,$user->uId,$user->aL);
  }
  else {
    //add workgroup entry to users work groups
    foreach ( $userToAddList as $u ) {
      $userGroupFile = 'Poi3dUserData/' . $u->uId . '/workGroups.json';
      //echo ($userGroupFile). "\n";
      iAddWorkGroupReference($userGroupFile,$data->gId,$grpProf->grNm,$user->uId,$user->aL);

      //Send email to invited users
      $uiLang="en";
      $eml="";
      if ( iGetUserLanguageAndEmail($u->aL, $u->uId, $uiLang, $eml, $errorText) == 0 ) {
        iSentGroupInvitationMail($eml, $grpProf->grNm, $uiLang, $errorText);
      }
    }
  }
  $errorText = "";

  //add workgroup entry to work group documents
  foreach ( $docsToAddList as $d ) {
    $docGroupFile = 'Poi3dUserContent/' . $d->uId . '/Docs/' . $d->dId . '/workGroups.json';
    //echo ($docGroupFile). "\n";
    iAddWorkGroupReference($docGroupFile,$data->gId,$grpProf->grNm,$grpProf->ownId,$grpProf->ownNm);
  }
  return 0;
}

function ReadWorkGroup ($data, &$ret, &$errorText)
{
  $ret = (object) [];
  $errorText = "";

  if ( empty ( $data->gDir ) ) {
    $errorText = "gDir not set";
    return 1;
  }

  if (iCheckGlobalAccess($data->gDir) == false) {

    if ( iLoadUserBaseData( $data, $user, $errorText ) ) {
      return 2;
    }

    if ( empty ( $user->docDir ) ) {
      $errorText = "docDir of user not set";
      return 3;
    }
  }

  $grpDirName = $data->gDir;
  $grpDocsFname = $grpDirName . '/documents.json';
  $grpUsersFname = $grpDirName . '/users.json';
  $wgProfileFname = $grpDirName . '/profile.json';

  $prof = (object) [];
  $prof = iGetJsonFromFile($wgProfileFname,true)->data;

  $docs = [];
  $docs = iGetJsonFromFile($grpDocsFname,true)->data;

  $users = [];
  if(file_exists($grpUsersFname)) {
    $u = iReadFile ( $grpUsersFname );
    $users = json_decode ( $u );
  }

  $ret->prof = $prof;
  $ret->docs = $docs;
  $ret->users = $users;

  return 0;
}

function LoadGroupAttachments ( $data, &$ret, &$errorText, &$srvInfo)
{
  $ret = (object) [];
  $srvInfo = (object)[];
  $errorText = "";

  if (iCheckGlobalAccess($data->dirName) == false) {
    if ( iLoadUserBaseData( $data, $user, $errorText ) ) {
      return 1;
    }

    if ( empty ( $data->dirName ) ) {
      $errorText = "dirName not set";
      return 2;
    }
  }

  $grpDirName = $data->dirName;
  $grpChatFname = $grpDirName . '/grpChat.json';

  $chat = [];
  $content = iGetJsonFromFile($grpChatFname,true);
  if(($content->isRemote == false)&&(!empty($content->data))) {
    $srvInfo->chatModDate = date(DATE_ATOM, filemtime($grpChatFname));
  }
  else {
    $srvInfo->chatModDate = date(DATE_ATOM, time());
  }
  $chat = $content->data;

  $ret->chat = $chat;

  return 0;
}

function UpdateWorkGroup ( $data, &$ret, &$errorText )
{
  $ret = [];
  $errorText = "";

  if ( iLoadUserBaseData( $data, $user, $errorText ) ) {
    return 1;
  }

  if ( empty ( $data->gDir ) ) {
    $errorText = "wgDir not set";
    return 2;
  }

  $userList = json_decode ( $data->users );
  if ( !is_array ( $userList ) ) {
    $errorText = "invalid data structure (users)";
    return 3;
  }

  $userToAddList = json_decode ( $data->usersToAdd );
  if ( !is_array ( $userToAddList ) ) {
    $errorText = "invalid data structure (usersToAdd)";
    return 4;
  }

  $userToDelList = json_decode ( $data->usersToDelete );
  if ( !is_array ( $userToDelList ) ) {
    $errorText = "invalid data structure (usersToDelete)";
    return 5;
  }
  //$data->docs return 6

  $docsToAddList = json_decode ( $data->docsToAdd );
  if ( !is_array ( $docsToAddList ) ) {
    $errorText = "invalid data structure (docsToAdd)";
    return 7;
  }

  $docsToDelList = json_decode ( $data->docsToDelete );
  if ( !is_array ( $docsToDelList ) ) {
    $errorText = "invalid data structure (docsToDelete)";
    return 8;
  }

  //path creation return 9

  $grpDirName = $data->gDir;
  $grpDocsFname = $grpDirName . '/documents.json';
  $grpUsersFname = $grpDirName . '/users.json';
  $grpProfileFile = $grpDirName . '/profile.json';
  $pathParts = pathinfo ( $grpUsersFname );

  //write meta data
  if ( !iWriteFile( $grpProfileFile, $data->prof ) ) {
    $errorText = "couldn't write" . $grpProfileFile;
    return 10;
  }

  // create doc list file
  if ( !iWriteFile( $grpDocsFname, $data->docs ) ) {
    $errorText = "couldn't write" . $grpDocsFname;
    return 11;
  }

  // create user list file
  if ( !iWriteFile( $grpUsersFname, $data->users ) ) {
    $errorText = "couldn't write" . $grpUsersFname;
    return 12;
  }

  $grpProf = json_decode ( $data->prof );

  //add workgroup entry to users work groups
  foreach ( $userToAddList as $u ) {
    $userGroupFile = 'Poi3dUserData/' . $u->uId . '/workGroups.json';
    iAddWorkGroupReference($userGroupFile,$data->gId,$grpProf->grNm,$user->uId,$user->aL);

    //Send email to invited users
    $uiLang="en";
    $eml="";
    if ( iGetUserLanguageAndEmail($u->aL, $u->uId, $uiLang, $eml, $errorText) == 0 ) {
      //echo  ( $u->uId ). "\n";
      iSentGroupInvitationMail($eml, $grpProf->grNm, $uiLang, $errorText);
    }
  }
  $errorText = "";

  //add workgroup entry to work group documents
  foreach ( $docsToAddList as $d ) {
    $docGroupFile = 'Poi3dUserContent/' . $d->uId . '/Docs/' . $d->dId . '/workGroups.json';
    iAddWorkGroupReference($docGroupFile,$data->gId,$grpProf->grNm,$grpProf->ownId,$grpProf->ownNm);
  }

  //remove workgroup entry from users work groups
  foreach ( $userToDelList as $u ) {
    $userGroupFile = 'Poi3dUserData/' . $u->uId . '/workGroups.json';
    //echo ($userGroupFile). "\n";
    iRemoveWorkGroupReference($userGroupFile,$data->gId);
  }

  //remove workgroup entry from work group documents
  foreach ( $docsToDelList as $d ) {
    $docGroupFile = 'Poi3dUserContent/' . $d->uId . '/Docs/' . $d->dId . '/workGroups.json';
    //echo ($docGroupFile). "\n";
    iRemoveWorkGroupReference($docGroupFile,$data->gId);
  }

  //delete group directory return 13
  return 0;
}

function DeleteWorkGroup ($data, &$ret, &$errorText)
{
  $ret = [];
  $errorText = "";

  if ( iLoadUserBaseData( $data, $user, $errorText ) ) {
    return 1;
  }

  $grpDirName = "";

  if(empty($data->gDir)) {
    if ( empty ( $user->docDir ) ) {
      $errorText = "docDir of user not set";
      return 2;
    }
    $grpDirName = $user->docDir . '/WorkGroups/' . $data->gId;
  }
  else
    $grpDirName = $data->gDir;

  $err = iRemoveWorkGroup($user, $data->gId, $grpDirName, "delete", $errorText);

  if( $err != 0){
    return $err+10;
  }

  return 0;
}
