Menu="UserList"
Title="Edit User"
Tag="user"
---
<?PHP
/* Copyright 2005-2023, Lime Technology
 * Copyright 2012-2023, Bergware International.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License version 2,
 * as published by the Free Software Foundation.
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 */
?>
<?
if (!array_key_exists($name, $users)) {
  echo "<script>done()</script>";
  exit;
}
$user   = "/boot/config/plugins/dynamix/users/$name.png";
$void   = "<img src='/webGui/images/user.png' width='48' height='48' id='image' onclick='$(&quot;#drop&quot;).click()' style='cursor:pointer' title='"._('Click to select PNG file')."'>";
$icon   = "<i class='fa fa-trash top' title='"._('Restore default image')."' onclick='restore()'></i>";
$zxcvbn = file_exists('/boot/config/plugins/dynamix/zxcvbn.js');

$file   = "/boot/config/ssh/root/authorized_keys";
$text   = preg_replace(["/\r\n/","/\r/"],"\n", @file_get_contents($file) ?: '');
?>
<script src="<?autov('/webGui/javascript/jquery.filedrop.js')?>"></script>
<?if ($zxcvbn):?>
<script src="<?autov('/boot/config/plugins/dynamix/zxcvbn.js')?>" async></script>
<?endif;?>
<script>
var path = '/boot/config/plugins/dynamix/users';
var filename = '';

function base64(str) {
  return window.btoa(unescape(encodeURIComponent(str)));
}
function showPassword() {
  if ($('#showPass').hasClass('checked')) {
    $('#showPass').removeClass('checked fa-eye-slash').addClass('fa-eye');
    var type = 'password';
  } else {
    $('#showPass').addClass('checked fa-eye-slash').removeClass('fa-eye');
    var type = 'text';
  }
  $('input[name="userPasswordGUI"]').attr('type',type);
  $('input[name="userPasswordConfGUI"]').attr('type',type);
}
function checkPassword(form) {
  if (form.userPasswordGUI.value.length > 128 || form.userPasswordConfGUI.value.length > 128) {
    swal({title:"_(Password too long)_",text:"_(Use a password up to 128 characters)_",type:"error",html:true,confirmButtonText:"_(Ok)_"});
    return false;
  }
  form.userPassword.value = base64(form.userPasswordGUI.value);
  form.userPasswordConf.value = base64(form.userPasswordConfGUI.value);
  form.userPasswordGUI.disabled = true;
  form.userPasswordConfGUI.disabled = true;
  return true;
}
function validatePassword(input) {
<?if ($zxcvbn):?>
  var custom = ['unraid','limetech','lime-technology','bergware','squidly'];
  var strength = ['Worst','Bad','Weak','Good','Strong'];
  var emoji = ['&#128565;','&#128553;','&#128532;','&#128512;','&#128526;'];
  if (!input) {
    $('#strength-bar').css('background-color','transparent');
    $('#strength-text').html('');
    $('.usage-disk.sys').addClass('none');
  } else {
    var bar = zxcvbn(input,custom);
    switch (bar.score) {
      case 0: $('#strength-bar').css('background-color','red'); break;
      case 1: $('#strength-bar').css('background-color','yellow'); break;
      case 2: $('#strength-bar').css('background-color','orange'); break;
      case 3: $('#strength-bar').css('background-color','blue'); break;
      case 4: $('#strength-bar').css('background-color','green'); break;
    }
    $('#strength-bar').css('width',Math.min(input.length*100/64,100)+'%');
    $('#strength-text').html(emoji[bar.score]+' '+strength[bar.score]+'. '+bar.feedback.warning);
    $('.usage-disk.sys').removeClass('none');
  }
<?endif;?>
}
function restore() {
  // restore original image and activate APPLY button
  $('#dropbox').html("<?=$void?>");
  $('input[name="userDesc"]').trigger('change');
  filename = 'reset';
}
function upload(remove) {
  // save or delete upload when APPLY is pressed
  if (remove || filename=='reset') {
    $.post("/webGui/include/FileUpload.php",{cmd:'delete',path:path,filename:'<?=addslashes(htmlspecialchars($name))?>.png'});
  } else if (filename) {
    $.post("/webGui/include/FileUpload.php",{cmd:'save',path:path,filename:filename,output:'<?=addslashes(htmlspecialchars($name))?>.png'});
  }
}
function checkKey(form) {
  const rows = form.text.value.split('\n');
  for (let i = 0; i < rows.length; i++) {
    const line = rows[i].trim();
    if (!line || line.startsWith('#')) continue;
    // Split options and key (optional options before key type)
    const optionPattern = /^((?:[a-zA-Z0-9\-]+(?:="[^"]*"|=[^",\s]+)?)(?:,(?:[a-zA-Z0-9\-]+(?:="[^"]*"|=[^",\s]+)?))*)\s+(ssh-[^\s]+|ecdsa-[^\s]+|sk-[^\s]+)\s+/;
    let optionsStr = null;
    let keyLine = line;
    const match = line.match(optionPattern);
    if (match) {
      optionsStr = match[1];
      const keyStart = line.indexOf(match[2]);
      keyLine = line.slice(keyStart);
    }
    // Validate options (if any)
    if (optionsStr) {
      const options = optionsStr.split(',');
      for (const opt of options) {
        const [key, val] = opt.split('=');
        if (key === 'from' && val) {
          const fromValue = val.replace(/^"|"$/g, ''); // strip quotes
          const entries = fromValue.split(',');
          const ipRegex = /^!?(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\/(?:3[0-2]|[12]?[0-9]))?$/;
          const ipv6Regex = /^!?(([a-fA-F0-9]{1,4}:){7}[a-fA-F0-9]{1,4}|(([a-fA-F0-9]{1,4}:){1,7}:)|(([a-fA-F0-9]{1,4}:){1,6}:[a-fA-F0-9]{1,4})|(([a-fA-F0-9]{1,4}:){1,5}(:[a-fA-F0-9]{1,4}){1,2})|(([a-fA-F0-9]{1,4}:){1,4}(:[a-fA-F0-9]{1,4}){1,3})|(([a-fA-F0-9]{1,4}:){1,3}(:[a-fA-F0-9]{1,4}){1,4})|(([a-fA-F0-9]{1,4}:){1,2}(:[a-fA-F0-9]{1,4}){1,5})|([a-fA-F0-9]{1,4}:)((:[a-fA-F0-9]{1,4}){1,6})|:((:[a-fA-F0-9]{1,4}){1,7}|:))(\/\d{1,3})?$/;
          const hostnameRegex = /^!?([a-zA-Z0-9*.-]+\.[a-zA-Z]{2,})$/;
          const wildcardIPRegex = /^!?(\d{1,3}\.){0,3}\*$/;
          for (const entryRaw of entries) {
            const entry = entryRaw.trim();
            if (
              !ipRegex.test(entry) &&
              !ipv6Regex.test(entry) &&
              !hostnameRegex.test(entry) &&
              !wildcardIPRegex.test(entry)
            ) {
              swal({
                title: "Invalid `from=` Option",
                text: `[Line ${i + 1}] Invalid from= entry: ${entry}`,
                type: "error",
                html: true,
                confirmButtonText: "OK"
              });
              return false;
            }
          }
        }
        // Additional option validation can go here
      }
    }
    // Validate SSH key
    const keyRegex = /^(ssh-(rsa|dss)|ecdsa-sha2-nistp(256|384|521)|sk-(ecdsa-sha2-nistp256|ssh-ed25519)@openssh\.com|ssh-ed25519) [A-Za-z0-9+/=]+(?: .*)?$/;
    if (!keyRegex.test(keyLine)) {
      const keyType = keyLine.split(' ')[0];
      swal({
        title: "Invalid Key Format",
        text: `[Line ${i + 1}] Invalid key or unsupported type: ${keyType}`,
        type: "error",
        html: true,
        confirmButtonText: "OK"
      });
      return false;
    }
  }
  return true;
}
$(function(){
  var dropbox = $('#dropbox');
  // attach the drag-n-drop feature to the 'dropbox' element
  dropbox.filedrop({
    maxfiles:1,
    maxfilesize:512, // KB
    url:'/webGui/include/FileUpload.php',
    data:{"csrf_token":"<?=$var['csrf_token']?>"},
    beforeEach:function(file) {
      if (!file.type.match(/^image\/png/)) {
        swal({title:"Warning",text:"_(Only PNG images are allowed)_!",type:"warning",html:true,confirmButtonText:"_(Ok)_"});
        return false;
      }
    },
    error: function(error, file, i) {
      switch (error) {
      case 'BrowserNotSupported':
        swal({title:"_(Browser error)_",text:"_(Your browser does not support HTML5 file uploads)_!",type:"error",html:true,confirmButtonText:"_(Ok)_"});
        break;
      case 'TooManyFiles':
        swal({title:"_(Too many files)_",text:"_(Please select one file only)_!",html:true,type:"error"});
        break;
      case 'FileTooLarge':
        swal({title:"_(File too large)_",text:"_(Maximum file upload size is 512K)_ (524,288 _(bytes)_)",type:"error",html:true,confirmButtonText:"_(Ok)_"});
        break;
      }
    },
    uploadStarted:function(i,file,count) {
      var image = $('img', $(dropbox));
      var reader = new FileReader();
      image.width = 48;
      image.height = 48;
      reader.onload = function(e){image.attr('src',e.target.result);};
      reader.readAsDataURL(file);
    },
    uploadFinished:function(i,file,response) {
      if (response == 'OK 200') {
        if (!filename || filename=='reset') $(dropbox).append("<?=$icon?>");
        $('input[name="userDesc"]').trigger('change');
        filename = file.name;
      } else {
        swal({title:"_(Upload error)_",text:response,type:"error",html:true,confirmButtonText:"_(Ok)_"});
      }
    }
  });
  // simulate a drop action when manual file selection is done
  $('#drop').bind('change', function(e) {
    var files = e.target.files;
    if ($('#dropbox').triggerHandler({type:'drop',dataTransfer:{files:files}})==false) e.stopImmediatePropagation();
  });
  // auto size the textarea
  $('form').find('textarea').on('input change',function(){
    $(this).prop('rows',Math.max(($(this).val().match(/\n/g)||[]).length+1,10));
    $('form').find('input[value="_(Save)_"]').prop('disabled',false);
  });
});
</script>
<div class="spinner fixed"></div>
<form markdown="1" method="POST" action="/update.htm" target="progressFrame" onsubmit="upload(<?=$name=="root" ? 'false' : 'this.confirmDelete.checked'?>)">
<input type="hidden" name="userName" value="<?=htmlspecialchars($name)?>">

_(User name)_:
: <?=htmlspecialchars($name)?>

_(Description)_:
: <input type="text" name="userDesc" maxlength="64" value="<?=htmlspecialchars($users[$name]['desc'])?>" pattern='[^&:"]*'>

:user_edit_description_help:

_(Custom image)_:
: <span>
    <span id="dropbox">
<?if (file_exists($user)):?>
  <img src="<?=autov($user)?>" id="image" width="48" height="48" onclick="$('#drop').click()" style="cursor:pointer" title="_(Click to select PNG file)_"><?=$icon?>
<?else:?>
  <?=$void?>
<?endif;?>
    </span>
  </span>
  <em>_(Drag-n-drop a PNG file or click the image at the left)_</em><input type="file" id="drop" accept="image/png" style="display:none">

:user_edit_custom_image_help:

<?if ($name=="root"):?>
&nbsp;
<?else:?>
_(Delete)_<input type="checkbox" name="confirmDelete" onChange="chkDelete(this.form, this.form.cmdUserEdit)">
<?endif;?>
: <span class="inline-block">
    <input type="submit" name="cmdUserEdit" value="_(Apply)_" onclick="if (this.value=='_(Delete)_') this.value='Delete'; else this.value='Apply';" disabled>
    <input type="button" value="_(Done)_" onclick="done('UserEdit')">
  </span>
</form>

<form markdown="1" method="POST" action="/update.htm" onsubmit="return checkPassword(this)" target="progressFrame">
<input type="hidden" name="userName" value="<?=htmlspecialchars($name)?>">

_(Password)_:
: <span class="flex flex-row items-center gap-2">
    <input type="hidden" name="userPassword" value="">
    <input type="password" name="userPasswordGUI" maxlength="129" autocomplete="new-password" onKeyUp="validatePassword(this.value);this.form.cmdUserEdit.disabled=(this.form.userPasswordGUI.value != this.form.userPasswordConfGUI.value);">
    <i id="showPass" class="fa fa-eye" style="cursor:pointer" title="_(Show / Hide password)_" onclick="showPassword()"></i>
    <span>
      <span class="usage-disk sys none">
        <span id="strength-bar" style="width:0"></span>
        <span></span>
      </span>
      <span id="strength-text"></span>
    </span>
  </span>

:user_password_help:

_(Retype password)_:
: <input type="hidden" name="userPasswordConf" value="">
  <input type="password" name="userPasswordConfGUI" maxlength="129" autocomplete="new-password" onKeyUp="this.form.cmdUserEdit.disabled=(this.form.userPasswordGUI.value != this.form.userPasswordConfGUI.value);">

&nbsp;
: <span class="inline-block">
    <input type="submit" name="cmdUserEdit" value="_(Change)_" onclick="this.value='Change'" disabled>
    <input type="button" value="_(Done)_" onclick="done('UserEdit')">
  </span>
</form>

<?if ($name == 'root'):?>
<form markdown="1" method="POST" action="/update.php" onsubmit="return checkKey(this)" target="progressFrame">
<input type="hidden" name="#include" value="/webGui/include/update.file.php">
<input type="hidden" name="#file" value="<?=$file;?>">

_(SSH authorized keys)_:
: <textarea class="font-mono" spellcheck="false" cols="80" rows="<?=max(substr_count($text,"\n")+1,10)?>" maxlength="16384" name="text"><?=htmlspecialchars($text)?></textarea>

&nbsp;
: <span class="inline-block">
    <input type="submit" value="_(Save)_" disabled>
    <input type="button" value="_(Done)_" onclick="done()">
  </span>

</form>
<?endif;?>

<?if ($name != 'root' && $var['shareSMBEnabled'] != 'no'):?>
<script>
var users = {};
var security = {};
var readList = {};
var writeList = {};
<?
$rw = 'read-write';
$ro = 'read-only';
$no = 'no-access';
foreach ($users as $user) {
  $idx = $user['idx'];
  if ($idx) echo "users[\"{$user['name']}\"]='$idx';\n";
}
foreach ($shares as $share => $data) {
  echo "security[\"$share\"]=\"{$sec[$share]['security']}\";\n";
  echo "readList[\"$share\"]=\"{$sec[$share]['readList']}\";\n";
  echo "writeList[\"$share\"]=\"{$sec[$share]['writeList']}\";\n";
}
?>
function updateAccess(form,data,n,i) {
  var name = "<?=$name?>";
  if (data) {
    if (n<i) {
      $.post('/update.htm',data[n], function(){setTimeout(function(){updateAccess(form,data,++n,i);},3000);});
    } else {
      $('div.spinner.fixed').hide();
      $('input[value="Reset"]').val('Done').prop('disabled',false).prop('onclick',null).off('click').click(function(){done('UserEdit');});
    }
  } else {
    var data = [], i = 0;
    $(form).find('select').each(function(){
      if ($(this).prop('id')) {
        var share = decodeURI($(this).prop('id'));
        var read = readList[share].split(',');
        var write = writeList[share].split(',');
        var access = '';
        data[i] = {};
        data[i]['shareName'] = share;
        data[i]['userAccess.0'] = '<?=$no?>';
        for (var user in users) {
          var idx = users[user];
          switch (security[share]) {
          case 'public':
            access = '<?=$rw?>';
            break;
          case 'secure':
            if (user == name) access = $(this).val();
            else access = write.includes(user) ? '<?=$rw?>' : '<?=$ro?>';
            break;
          case 'private':
            if (user == name) access = $(this).val();
            else access = write.includes(user) ? '<?=$rw?>' : (read.includes(user) ? '<?=$ro?>' : '<?=$no?>');
            break;
          }
          data[i]['userAccess.'+idx] = access;
        }
        data[i]['changeShareAccess'] = 'Apply';
        i++;
      }
    });
    $(form).find('input').prop('disabled',true);
    $('div.spinner.fixed').show();
    updateAccess(form,data,0,i);
  }
}
</script>
<form markdown="1" method="POST">
<?
echo "<table class='unraid'>";
echo "<thead><tr><td>"._('Share')."</td><td>"._('Security')."</td><td>"._('User Access')."</td></tr></thead>";
echo "<tbody>";
foreach ($shares as $share => $data) {
  if ($sec[$share]['export']=='-') continue;
  $security = $sec[$share]['security'];
  $read = in_array($name,explode(',',$sec[$share]['readList']));
  $write = in_array($name,explode(',',$sec[$share]['writeList']));
  switch ($security) {
  case 'public' : $access = $rw; break;
  case 'secure' : $access = $write ? $rw : $ro; break;
  case 'private': $access = $write ? $rw : ($read ? $ro : $no); break;}
  echo "<tr><td>$share</td><td>"._(ucfirst($security))."</td><td><select onchange='$(this).prop(\"id\",\"".rawurlencode($share)."\")'>";
  echo mk_option($access,$rw,_('Read/Write'));
  if ($security!='public') echo mk_option($access,$ro,_('Read-only'));
  if ($security=='private') echo mk_option($access,$no,_('No Access'));
  echo "</select></td></tr>";
}
echo "</tbody>";
echo "</table>";
?>

&nbsp;
: <span class="inline-block">
    <input type="button" value="_(Apply)_" onclick="updateAccess(this.form)" disabled>
    <input type="button" value="_(Done)_" onclick="done('UserEdit')">
  </span>
</form>
<?endif;?>
