mirror of
https://github.com/the-djmaze/snappymail.git
synced 2024-09-20 15:45:55 +08:00
Improved ACL handling #157
This commit is contained in:
parent
7e304daee0
commit
306d3ec7b5
|
@ -402,8 +402,6 @@ export class FolderModel extends AbstractModel {
|
|||
}
|
||||
),
|
||||
|
||||
canBeEdited: () => !this.type() && this.exists/* && this.selectable()*/,
|
||||
|
||||
isSystemFolder: () => this.type()
|
||||
| (FolderUserStore.allowKolab() && !!this.kolabType() & !SettingsUserStore.unhideKolabFolders()),
|
||||
|
||||
|
@ -484,8 +482,7 @@ export class FolderModel extends AbstractModel {
|
|||
}
|
||||
|
||||
edit() {
|
||||
// this.canBeEdited() && this.editing(true);
|
||||
this.canBeEdited() && showScreenPopup(FolderPopupView, [this]);
|
||||
showScreenPopup(FolderPopupView, [this]);
|
||||
}
|
||||
|
||||
unedit() {
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
}
|
||||
|
||||
.folder-name {
|
||||
cursor: pointer;
|
||||
word-break: break-all;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
@ -24,10 +25,6 @@
|
|||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.folder-name.can-be-edited {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
select {
|
||||
width: auto;
|
||||
}
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import { addObservablesTo } from 'External/ko';
|
||||
|
||||
import { AbstractViewPopup } from 'Knoin/AbstractViews';
|
||||
|
||||
import { addObservablesTo } from 'External/ko';
|
||||
import Remote from 'Remote/User/Fetch';
|
||||
import { FolderUserStore } from 'Stores/User/Folder';
|
||||
|
||||
export class FolderPopupView extends AbstractViewPopup {
|
||||
constructor() {
|
||||
|
@ -10,7 +9,7 @@ export class FolderPopupView extends AbstractViewPopup {
|
|||
addObservablesTo(this, {
|
||||
folder: null // FolderModel
|
||||
});
|
||||
|
||||
this.ACLAllowed = FolderUserStore.hasCapability('ACL');
|
||||
this.ACL = ko.observableArray();
|
||||
}
|
||||
|
||||
|
@ -26,19 +25,14 @@ export class FolderPopupView extends AbstractViewPopup {
|
|||
|
||||
beforeShow(folder) {
|
||||
this.ACL([]);
|
||||
folder.editing(true);
|
||||
this.folder(folder);
|
||||
Remote.request('FolderACL', (iError, data) => {
|
||||
this.ACLAllowed && Remote.request('FolderACL', (iError, data) => {
|
||||
if (!iError && data.Result) {
|
||||
this.ACL(
|
||||
Object.entries(data.Result).map(([key, value]) => {
|
||||
value.identifier = key;
|
||||
return value;
|
||||
})
|
||||
);
|
||||
this.ACL(Object.values(data.Result));
|
||||
}
|
||||
}, {
|
||||
folder: folder.fullName
|
||||
});
|
||||
!folder.type() && folder.exists && folder.selectable() && folder.editing(true);
|
||||
this.folder(folder);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,68 +26,82 @@ trait ACL
|
|||
*/
|
||||
public function ACLAllow(string $sFolderName, string $command) : bool
|
||||
{
|
||||
if ($this->ACLDisabled) {
|
||||
if ($this->ACLDisabled || !$this->hasCapability('ACL')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// The "RIGHTS=" capability MUST NOT include any of the rights defined in RFC 2086:
|
||||
// "l", "r", "s", "w", "i", "p", "a", "c", "d", and the digits ("0" .. "9")
|
||||
// So it is: RIGHTS=texk
|
||||
$mainRights = \str_split($this->CapabilityValue('RIGHTS') ?: '');
|
||||
if ($this->hasCapability('ACL') || $mainRights) {
|
||||
if ('MYRIGHTS' === $command) {
|
||||
// at least one of the "l", "r", "i", "k", "x", "a" rights is required
|
||||
return true;
|
||||
}
|
||||
$rights = $this->FolderMyRights($sFolderName);
|
||||
if ($rights) {
|
||||
switch ($command)
|
||||
{
|
||||
case 'LIST':
|
||||
case 'LSUB':
|
||||
return $rights->hasRight('LOOKUP');
|
||||
case 'CREATE':
|
||||
return true; // $parent->$rights->hasRight('k');
|
||||
case 'DELETE':
|
||||
return $rights->hasRight('x');
|
||||
case 'RENAME':
|
||||
return $rights->hasRight('k') && $rights->hasRight('x');
|
||||
case 'SELECT':
|
||||
case 'EXAMINE':
|
||||
case 'STATUS':
|
||||
return $rights->hasRight('r');
|
||||
case 'APPEND':
|
||||
case 'COPY':
|
||||
return $rights->hasRight('i');
|
||||
case 'EXPUNGE':
|
||||
return $rights->hasRight('e');
|
||||
// $mainRights = \str_split($this->CapabilityValue('RIGHTS') ?: '');
|
||||
|
||||
if ('MYRIGHTS' === $command) {
|
||||
// at least one of the "l", "r", "i", "k", "x", "a" rights is required
|
||||
return true;
|
||||
}
|
||||
|
||||
if (\in_array($command, ['GETACL','SETACL','LISTRIGHTS','DELETEACL'])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$rights = $this->FolderMyRights($sFolderName);
|
||||
if ($rights) {
|
||||
switch ($command)
|
||||
{
|
||||
case 'LIST':
|
||||
case 'LSUB':
|
||||
return $rights->hasRight('LOOKUP');
|
||||
case 'CREATE':
|
||||
return true; // $parent->$rights->hasRight('k');
|
||||
case 'DELETE':
|
||||
return $rights->hasRight('x');
|
||||
case 'RENAME':
|
||||
return $rights->hasRight('k') && $rights->hasRight('x');
|
||||
case 'SELECT':
|
||||
case 'EXAMINE':
|
||||
case 'STATUS':
|
||||
return $rights->hasRight('r');
|
||||
case 'APPEND':
|
||||
case 'COPY':
|
||||
return $rights->hasRight('i');
|
||||
case 'EXPUNGE':
|
||||
return $rights->hasRight('e');
|
||||
/*
|
||||
case 'SUBSCRIBE':
|
||||
return $rights->hasRight('l') || true;
|
||||
case 'UNSUBSCRIBE':
|
||||
return true;
|
||||
case 'CLOSE':
|
||||
return $rights->hasRight('e') || true;
|
||||
case 'FETCH':
|
||||
return $rights->hasRight('s') || true;
|
||||
case 'STORE':
|
||||
return $rights->hasRight('s') || $rights->hasRight('w') || $rights->hasRight('t');
|
||||
case 'SUBSCRIBE':
|
||||
return $rights->hasRight('l') || true;
|
||||
case 'UNSUBSCRIBE':
|
||||
return true;
|
||||
case 'CLOSE':
|
||||
return $rights->hasRight('e') || true;
|
||||
case 'FETCH':
|
||||
return $rights->hasRight('s') || true;
|
||||
case 'STORE':
|
||||
return $rights->hasRight('s') || $rights->hasRight('w') || $rights->hasRight('t');
|
||||
*/
|
||||
case 'GETACL':
|
||||
case 'SETACL':
|
||||
case 'LISTRIGHTS':
|
||||
case 'DELETEACL':
|
||||
return $rights->hasRight('a');
|
||||
}
|
||||
case 'GETACL':
|
||||
case 'SETACL':
|
||||
case 'LISTRIGHTS':
|
||||
case 'DELETEACL':
|
||||
return $rights->hasRight('a');
|
||||
}
|
||||
}
|
||||
return !\in_array($command, ['GETACL','SETACL','LISTRIGHTS','DELETEACL','MYRIGHTS']);
|
||||
return true;
|
||||
}
|
||||
|
||||
private function FolderACLRequest(string $sFolderName, string $sCommand, array $aParams) : \MailSo\Imap\ResponseCollection
|
||||
{
|
||||
if ($this->ACLAllow($sFolderName, $sCommand)) try {
|
||||
return $this->SendRequestGetResponse($sCommand, $aParams);
|
||||
} catch (\Throwable $oException) {
|
||||
// Error in IMAP command $sCommand: ACLs disabled
|
||||
$this->ACLDisabled = true;
|
||||
throw $oException;
|
||||
}
|
||||
}
|
||||
|
||||
public function FolderSetACL(string $sFolderName, string $sIdentifier, string $sAccessRights) : void
|
||||
{
|
||||
// if ($this->ACLAllow($sFolderName, 'SETACL')) {
|
||||
$this->SendRequestGetResponse('SETACL', array(
|
||||
$this->FolderACLRequest($sFolderName, 'SETACL', array(
|
||||
$this->EscapeFolderName($sFolderName),
|
||||
$this->EscapeString($sIdentifier),
|
||||
$this->EscapeString($sAccessRights)
|
||||
|
@ -96,8 +110,7 @@ trait ACL
|
|||
|
||||
public function FolderDeleteACL(string $sFolderName, string $sIdentifier) : void
|
||||
{
|
||||
// if ($this->ACLAllow($sFolderName, 'DELETEACL')) {
|
||||
$this->SendRequestGetResponse('DELETEACL', array(
|
||||
$this->FolderACLRequest($sFolderName, 'DELETEACL', array(
|
||||
$this->EscapeFolderName($sFolderName),
|
||||
$this->EscapeString($sIdentifier)
|
||||
));
|
||||
|
@ -105,9 +118,8 @@ trait ACL
|
|||
|
||||
public function FolderGetACL(string $sFolderName) : array
|
||||
{
|
||||
// if ($this->ACLAllow($sFolderName, 'GETACL')) {
|
||||
$oResponses = $this->SendRequestGetResponse('GETACL', array($this->EscapeFolderName($sFolderName)));
|
||||
$aResult = array();
|
||||
$oResponses = $this->FolderACLRequest($sFolderName, 'GETACL', array($this->EscapeFolderName($sFolderName)));
|
||||
foreach ($oResponses as $oResponse) {
|
||||
// * ACL INBOX.shared demo@snappymail.eu akxeilprwtscd foobar@snappymail.eu akxeilprwtscd demo2@snappymail.eu lrwstipekxacd
|
||||
if (\MailSo\Imap\Enumerations\ResponseType::UNTAGGED === $oResponse->ResponseType
|
||||
|
@ -118,7 +130,10 @@ trait ACL
|
|||
{
|
||||
$c = \count($oResponse->ResponseList);
|
||||
for ($i = 3; $i < $c; $i += 2) {
|
||||
$aResult[$oResponse->ResponseList[$i]] = new ACLResponse(\str_split($oResponse->ResponseList[$i+1]));
|
||||
$aResult[] = new ACLResponse(
|
||||
$oResponse->ResponseList[$i],
|
||||
$oResponse->ResponseList[$i+1]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -127,8 +142,7 @@ trait ACL
|
|||
|
||||
public function FolderListRights(string $sFolderName, string $sIdentifier) : ?ACLResponse
|
||||
{
|
||||
// if ($this->ACLAllow($sFolderName, 'LISTRIGHTS')) {
|
||||
$oResponses = $this->SendRequestGetResponse('LISTRIGHTS', array(
|
||||
$oResponses = $this->FolderACLRequest($sFolderName, 'LISTRIGHTS', array(
|
||||
$this->EscapeFolderName($sFolderName),
|
||||
$this->EscapeString($sIdentifier)
|
||||
));
|
||||
|
@ -140,7 +154,10 @@ trait ACL
|
|||
&& $sIdentifier === $oResponse->ResponseList[3]
|
||||
)
|
||||
{
|
||||
return static::aclRightsToClass(\array_slice($oResponse->ResponseList, 4));
|
||||
foreach (\array_slice($oResponse->ResponseList, 4) as $rule) {
|
||||
$result = \array_merge($result, \str_split($rule));
|
||||
}
|
||||
return new ACLResponse($sIdentifier, \implode('', \array_unique($result)));
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
@ -148,35 +165,17 @@ trait ACL
|
|||
|
||||
public function FolderMyRights(string $sFolderName) : ?ACLResponse
|
||||
{
|
||||
// if ($this->ACLAllow($sFolderName, 'MYRIGHTS')) {
|
||||
// if ($this->hasCapability('ACL')) {
|
||||
try {
|
||||
$oResponses = $this->SendRequestGetResponse('MYRIGHTS', array($this->EscapeFolderName($sFolderName)));
|
||||
foreach ($oResponses as $oResponse) {
|
||||
if (\MailSo\Imap\Enumerations\ResponseType::UNTAGGED === $oResponse->ResponseType
|
||||
&& isset($oResponse->ResponseList[3])
|
||||
&& 'MYRIGHTS' === $oResponse->ResponseList[1]
|
||||
&& $sFolderName === $oResponse->ResponseList[2]
|
||||
)
|
||||
{
|
||||
return static::aclRightsToClass(\array_slice($oResponse->ResponseList, 3));
|
||||
}
|
||||
$oResponses = $this->FolderACLRequest($sFolderName, 'MYRIGHTS', array($this->EscapeFolderName($sFolderName)));
|
||||
foreach ($oResponses as $oResponse) {
|
||||
if (\MailSo\Imap\Enumerations\ResponseType::UNTAGGED === $oResponse->ResponseType
|
||||
&& isset($oResponse->ResponseList[3])
|
||||
&& 'MYRIGHTS' === $oResponse->ResponseList[1]
|
||||
&& $sFolderName === $oResponse->ResponseList[2]
|
||||
)
|
||||
{
|
||||
return new ACLResponse('', $oResponse->ResponseList[3]);
|
||||
}
|
||||
} catch (\Throwable $oException) {
|
||||
// \MailSo\Imap\Exceptions\NegativeResponseException: Error in IMAP command MYRIGHTS: ACLs disabled
|
||||
$this->ACLDisabled = true;
|
||||
throw $oException;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static function aclRightsToClass(array $rules) : ACLResponse
|
||||
{
|
||||
$result = array();
|
||||
foreach ($rules as $rule) {
|
||||
$result = \array_merge($result, \str_split($rule));
|
||||
}
|
||||
return new ACLResponse(\array_unique($result));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -576,7 +576,7 @@ trait Folders
|
|||
}
|
||||
/*
|
||||
// RFC 4314
|
||||
if ($this->hasCapability('ACL') || $this->CapabilityValue('RIGHTS')) {
|
||||
if ($this->hasCapability('ACL')) {
|
||||
foreach ($oFolderCollection as $oFolder) {
|
||||
if ($oFolder->Selectable()) try {
|
||||
$oFolder->myRights = $this->FolderMyRights($oFolder->FullName);
|
||||
|
|
|
@ -17,18 +17,26 @@ namespace MailSo\Imap\Responses;
|
|||
*/
|
||||
class ACL implements \JsonSerializable
|
||||
{
|
||||
private array $rights;
|
||||
private string
|
||||
$identifier,
|
||||
$rights;
|
||||
|
||||
function __construct(array $rights)
|
||||
function __construct(string $identifier, string $rights)
|
||||
{
|
||||
$this->identifier = $identifier;
|
||||
$this->rights = $rights;
|
||||
}
|
||||
|
||||
public function identifier() : string
|
||||
{
|
||||
return $this->identifier;
|
||||
}
|
||||
|
||||
/** PHP 8.1
|
||||
public function hasRight(string|\MailSo\Imap\Enumerations\FolderACL $right)
|
||||
{
|
||||
if ($right instanceof \BackedEnum) {
|
||||
return \in_array($right->value, $this->rights);
|
||||
return \str_contains($this->rights, $right->value);
|
||||
}
|
||||
*/
|
||||
public function hasRight(string $right) : bool
|
||||
|
@ -37,13 +45,15 @@ class ACL implements \JsonSerializable
|
|||
if (\defined($const)) {
|
||||
$right = \constant($const);
|
||||
}
|
||||
return \in_array($right, $this->rights);
|
||||
return \str_contains($this->rights, $right);
|
||||
}
|
||||
|
||||
#[\ReturnTypeWillChange]
|
||||
public function jsonSerialize()
|
||||
{
|
||||
return [
|
||||
'identifier' => $this->identifier,
|
||||
'rights' => $this->rights,
|
||||
'mayReadItems' => ($this->hasRight('l') && $this->hasRight('r')),
|
||||
'mayAddItems' => $this->hasRight('i'),
|
||||
'mayRemoveItems' => ($this->hasRight('t') && $this->hasRight('e')),
|
||||
|
@ -52,8 +62,7 @@ class ACL implements \JsonSerializable
|
|||
'mayCreateChild' => $this->hasRight('k'),
|
||||
'mayRename' => $this->hasRight('x'),
|
||||
'mayDelete' => $this->hasRight('x'),
|
||||
'maySubmit' => $this->hasRight('p'),
|
||||
'raw' => \implode('', $this->rights)
|
||||
'maySubmit' => $this->hasRight('p')
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
</header>
|
||||
<form id="folderform" class="modal-body form-horizontal" autocomplete="off" spellcheck="false" data-bind="submit: submitForm">
|
||||
<div data-bind="with: folder">
|
||||
<div class="control-group">
|
||||
<div class="control-group" data-bind="if: editing">
|
||||
<label data-i18n="GLOBAL/NAME"></label>
|
||||
<input name="folder" type="text"
|
||||
autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"
|
||||
|
@ -30,7 +30,7 @@
|
|||
}"></div>
|
||||
<!-- /ko -->
|
||||
</div>
|
||||
<div class="control-group" data-bind="if: ACL">
|
||||
<div class="control-group" data-bind="if: ACLAllowed">
|
||||
<label>ACL</label>
|
||||
<div data-bind="foreach: ACL">
|
||||
<details>
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
<tr data-bind="css: { 'selectable': selectable, 'system': isSystemFolder }">
|
||||
<td data-bind="css: 'deep-' + deep">
|
||||
<span class="folder-name" data-bind="text: name, css: { 'can-be-edited': canBeEdited },
|
||||
click: edit,
|
||||
tooltipErrorTip: errorMsg"></span>
|
||||
<span class="folder-name" data-bind="text: name, click: edit, tooltipErrorTip: errorMsg"></span>
|
||||
<span class="folder-system-name" data-bind="text: nameInfo, visible: isSystemFolder"></span>
|
||||
<span class="folder-size" data-bind="text: friendlySize, visible: size"></span>
|
||||
</td>
|
||||
|
|
Loading…
Reference in a new issue