Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 22 additions & 3 deletions core/functions/nodes.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,12 @@ function makeHTML($indent, $parent, $expandAll, $hereid = '')
}
$mgrRole = (isset ($_SESSION['mgrRole']) && (string)$_SESSION['mgrRole'] === '1') ? '1' : '0';

$docgrp_cond = $docgrp ? 'OR dg.document_group IN (' . $docgrp . ')' : '';
// documents this user may work with, used to tell which nodes can take a new child
$usePermissions = $modx->getConfig('use_udperms');
$accessibleDocuments = \EvolutionCMS\Legacy\Permissions::getAccessibleDocumentIds();

// $docgrp_cond = $docgrp ? 'OR dg.document_group IN (' . $docgrp . ')' : '';
$mgrRole = (int)$mgrRole;
$docgrp_cond = $docgrp_cond;

$result = \EvolutionCMS\Models\SiteContent::query()->withTrashed()->select('site_content.id', 'site_content.pagetitle', 'longtitle',
'menutitle', 'parent', 'isfolder'
Expand Down Expand Up @@ -123,6 +126,18 @@ function makeHTML($indent, $parent, $expandAll, $hereid = '')
if ($mgrRole == 1 || $row['privatemgr'] == 0) {
$row['hasAccess'] = 1;
}
// reachability is wider than hasAccess: it also covers the private documents
// this user is a member of, which are legitimate places to create a resource in
$row['canAddChild'] = \EvolutionCMS\Support\ResourceParentGuard::nodeAcceptsChild(
\EvolutionCMS\Support\ResourceParentGuard::documentIsAccessible(
$mgrRole,
$usePermissions,
$row['privatemgr'],
$row['id'],
$accessibleDocuments
),
$row['deleted']
) ? 1 : 0;
$node = '';
$nodetitle = getNodeTitle($nodeNameSource, $row);
$nodetitleDisplay = $nodetitle;
Expand Down Expand Up @@ -243,7 +258,8 @@ function makeHTML($indent, $parent, $expandAll, $hereid = '')
'level' => $level,
'isPrivate' => 0,
'roles' => ($row['roles'] ? $row['roles'] : ''),
'nomove' => 0
'nomove' => 0,
'canAddChild' => $row['canAddChild']
];

$ph = $data;
Expand Down Expand Up @@ -665,6 +681,7 @@ function getTplSingleNode()
data-private="[+isPrivate+]"
data-roles="[+roles+]"
data-nomove="[+nomove+]"
data-canaddchild="[+canAddChild+]"
data-level="[+level+]"
data-treepageclick="[+tree_page_click+]"
[+contextmenu+]
Expand Down Expand Up @@ -696,6 +713,7 @@ function getTplFolderNode()
data-private="[+isPrivate+]"
data-roles="[+roles+]"
data-nomove="[+nomove+]"
data-canaddchild="[+canAddChild+]"
data-level="[+level+]"
data-icon-expanded="[+tree_plusnode+]"
data-icon-collapsed="[+tree_minusnode+]"
Expand Down Expand Up @@ -738,6 +756,7 @@ function getTplFolderNodeNotChildren()
data-private="[+isPrivate+]"
data-roles="[+roles+]"
data-nomove="[+nomove+]"
data-canaddchild="[+canAddChild+]"
data-level="[+level+]"
data-icon-expanded="[+tree_plusnode+]"
data-icon-collapsed="[+tree_minusnode+]"
Expand Down
186 changes: 154 additions & 32 deletions core/src/Legacy/Permissions.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
<?php namespace EvolutionCMS\Legacy;

use EvolutionCMS\Models\DocumentGroup;
use EvolutionCMS\Models\SiteContent;
use EvolutionCMS\Support\ResourceParentGuard;

/**
* @class: udperms
Expand Down Expand Up @@ -29,32 +31,24 @@ class Permissions
*/
public function checkPermissions()
{

global $udperms_allowroot;
$modx = evo();

$document = $this->document;
$role = $this->role;
$document = (int) $this->document;

if ($role == 1) {
return true; // administrator - grant all document permissions
if (ResourceParentGuard::grantsWithoutLookup($this->role, $modx->getConfig('use_udperms'))) {
return true; // administrator, or permissions aren't in use
}

if ($modx->getConfig('use_udperms') == 0 || $modx->getConfig('use_udperms') == "" || !isset($modx->config['use_udperms'])) {
return true; // permissions aren't in use
}
$parent = SiteContent::query()->find($this->document);
$parent = $parent->parent ?? null;
if ($document == 0 && $parent == null && $udperms_allowroot == 1) {
return true;
} // User is allowed to create new document in root
if (($this->duplicateDoc == true || $document == 0) && $parent == 0 && $udperms_allowroot == 0) {
return false; // deny duplicate || create new document at root if Allow Root is No
if ($document === 0) {
return static::rootIsAllowed(); // placing a resource in the site root
}

// get document groups for current user
$docgrp = empty($_SESSION['mgrDocgroups']) ? '' : implode(' || dg.document_group = ',
$_SESSION['mgrDocgroups']);
if ($this->duplicateDoc) {
$source = SiteContent::withTrashed()->find($document);
if (ResourceParentGuard::duplicateBlockedAtRoot(static::allowRootSetting(), $source->parent ?? 0)) {
return false; // the duplicate would end up in the root
}
}

/* Note:
A document is flagged as private whenever the document group that it
Expand All @@ -64,22 +58,150 @@ public function checkPermissions()
are private to the manager users will not be private to web users if the
document group is not assigned to a web user group and visa versa.
*/
$permissionsok = false; // set permissions to false

$query = SiteContent::query()->select('id');
if(!empty($docgrp)){
$query = $query->leftJoin('document_groups', 'site_content.id','=', 'document_groups.document')
->where(function($q) use ($docgrp) {
$q->where('document_groups.document_group', $docgrp)
->orWhere('site_content.privatemgr', 0);
return static::documentIsAccessible($document);
}

/**
* The manager no longer exports settings as globals, so the setting is the authoritative
* source here; the legacy global is still honoured when something did define it.
*
* @return mixed
*/
public static function allowRootSetting()
{
global $udperms_allowroot;

return isset($udperms_allowroot) ? $udperms_allowroot : evo()->getConfig('udperms_allowroot');
}

/**
* @return bool
*/
public static function rootIsAllowed()
{
return ResourceParentGuard::allowsRoot(static::allowRootSetting());
}

/**
* Document groups of the manager user of the current session.
*
* @return array
*/
public static function getManagerDocumentGroups()
{
return (isset($_SESSION['mgrDocgroups']) && is_array($_SESSION['mgrDocgroups']))
? $_SESSION['mgrDocgroups']
: [];
}

/**
* Ids of the private documents the manager user of the current session belongs to.
* Trashed documents are kept, so restoring one from the recycle bin stays possible.
*
* @return array
*/
public static function getAccessibleDocumentIds()
{
static $documents = null;

if ($documents === null) {
$docgrp = static::getManagerDocumentGroups();
$documents = empty($docgrp)
? []
: DocumentGroup::query()->whereIn('document_group', $docgrp)
->pluck('document')
->map(static function ($document) {
return (int) $document;
})
->all();
}

return $documents;
}

/**
* Whether the manager user of the current session may work with the given document.
*
* @param int $document
* @return bool
*/
public static function documentIsAccessible($document)
{
$docgrp = static::getManagerDocumentGroups();

// withTrashed(), so publishing, restoring and moving a trashed document keep working
$query = SiteContent::withTrashed()->where('site_content.id', (int) $document);

if (empty($docgrp)) {
$query->where('site_content.privatemgr', 0);
} else {
$query->leftJoin('document_groups', 'site_content.id', '=', 'document_groups.document')
->where(function ($q) use ($docgrp) {
$q->where('site_content.privatemgr', 0)
->orWhereIn('document_groups.document_group', $docgrp);
});
}else {
$query->where('privatemgr', 0);
}
if ($query->count() > 0) {
$permissionsok = true;

return $query->exists();
}

/**
* Whether the manager user of the current session may place a resource inside $parent.
*
* @param int $parent
* @return bool
*/
public static function canCreateIn($parent)
{
$udperms = new static();
$udperms->user = evo()->getLoginUserID('mgr');
$udperms->document = (int) $parent;
$udperms->role = $_SESSION['mgrRole'] ?? 0;

return $udperms->checkPermissions();
}

/**
* First location the manager user of the current session may create resources in.
*
* @return int
*/
public static function getFirstAllowedParent()
{
if (static::canCreateIn(0)) {
return 0;
}

return $permissionsok;
return ResourceParentGuard::pickDefaultParent(false, static::findFirstAccessibleDocument());
}

/**
* Topmost document of the tree the manager user of the current session can reach.
*
* @return int|null
*/
protected static function findFirstAccessibleDocument()
{
$docgrp = static::getManagerDocumentGroups();

$query = SiteContent::query()->select('site_content.id')
->where('site_content.deleted', 0);

if (empty($docgrp)) {
$query->where('site_content.privatemgr', 0);
} else {
$query->leftJoin('document_groups', 'site_content.id', '=', 'document_groups.document')
->where(function ($q) use ($docgrp) {
$q->where('site_content.privatemgr', 0)
->orWhereIn('document_groups.document_group', $docgrp);
});
}

$first = $query->orderBy('site_content.parent')
->orderBy('site_content.menuindex')
->orderBy('site_content.id')
->first();

return $first === null ? null : (int) $first->id;
}
}
105 changes: 105 additions & 0 deletions core/src/Support/ResourceParentGuard.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
<?php

namespace EvolutionCMS\Support;

/**
* Decisions behind "may this manager user put a resource here?".
*
* Kept free of database and session access so both the permission check and the
* resource tree can share exactly the same rules.
*/
class ResourceParentGuard
{
/**
* Access can be granted without looking at any document group.
*
* @param int|string|null $role manager role of the user
* @param mixed $usePermissions the use_udperms setting
* @return bool
*/
public static function grantsWithoutLookup($role, $usePermissions): bool
{
return (int)$role === 1 || !$usePermissions;
}

/**
* Whether resources may be placed in the site root.
*
* @param mixed $allowRootSetting the udperms_allowroot setting
* @return bool
*/
public static function allowsRoot($allowRootSetting): bool
{
return (int)$allowRootSetting === 1;
}

/**
* Duplicating a document whose parent is the root produces a copy in the root,
* which is denied whenever the root itself is denied.
*
* @param mixed $allowRootSetting
* @param int|string|null $sourceParent parent of the document being duplicated
* @return bool
*/
public static function duplicateBlockedAtRoot($allowRootSetting, $sourceParent): bool
{
return !static::allowsRoot($allowRootSetting) && (int)$sourceParent === 0;
}

/**
* Whether a document is reachable for a user, given the private documents that
* user is a member of.
*
* @param int|string|null $role
* @param mixed $usePermissions
* @param int|string|null $privatemgr
* @param int|string|null $documentId
* @param array $accessibleDocumentIds
* @return bool
*/
public static function documentIsAccessible(
$role,
$usePermissions,
$privatemgr,
$documentId,
array $accessibleDocumentIds
): bool {
if (static::grantsWithoutLookup($role, $usePermissions)) {
return true;
}

return (int)$privatemgr === 0 || in_array((int)$documentId, $accessibleDocumentIds, true);
}

/**
* Whether a resource tree node may receive a new child. Trashed documents are
* excluded so nothing is created inside the recycle bin.
*
* @param mixed $hasAccess
* @param int|string|null $deleted
* @return bool
*/
public static function nodeAcceptsChild($hasAccess, $deleted): bool
{
return (bool)$hasAccess && (int)$deleted === 0;
}

/**
* Where a new resource should be created when the caller did not name a parent:
* the root when it is available, otherwise the first document the user can reach.
* Falls back to the root so the form still opens and the save processor is the one
* reporting the missing permission.
*
* @param bool $rootAllowed
* @param int|string|null $firstAccessibleDocument
* @return int
*/
public static function pickDefaultParent(bool $rootAllowed, $firstAccessibleDocument): int
{
if ($rootAllowed) {
return 0;
}

return $firstAccessibleDocument === null ? 0 : (int)$firstAccessibleDocument;
}
}
Loading