PageAuthorsTrait.php 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * @package Grav\Framework\Flex
  5. *
  6. * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
  7. * @license MIT License; see LICENSE file for details.
  8. */
  9. namespace Grav\Framework\Flex\Pages\Traits;
  10. use Grav\Common\User\Interfaces\UserInterface;
  11. use Grav\Framework\Acl\Access;
  12. use InvalidArgumentException;
  13. use function in_array;
  14. use function is_array;
  15. use function is_bool;
  16. use function is_string;
  17. /**
  18. * Trait PageAuthorsTrait
  19. * @package Grav\Framework\Flex\Pages\Traits
  20. */
  21. trait PageAuthorsTrait
  22. {
  23. /** @var array<int,UserInterface> */
  24. private $_authors;
  25. /** @var array|null */
  26. private $_permissionsCache;
  27. /**
  28. * Returns true if object has the named author.
  29. *
  30. * @param string $username
  31. * @return bool
  32. */
  33. public function hasAuthor(string $username): bool
  34. {
  35. $authors = (array)$this->getNestedProperty('header.permissions.authors');
  36. if (empty($authors)) {
  37. return false;
  38. }
  39. foreach ($authors as $author) {
  40. if ($username === $author) {
  41. return true;
  42. }
  43. }
  44. return false;
  45. }
  46. /**
  47. * Get list of all author objects.
  48. *
  49. * @return array<int,UserInterface>
  50. */
  51. public function getAuthors(): array
  52. {
  53. if (null === $this->_authors) {
  54. $this->_authors = $this->loadAuthors($this->getNestedProperty('header.permissions.authors', []));
  55. }
  56. return $this->_authors;
  57. }
  58. /**
  59. * @param bool $inherit
  60. * @return array
  61. */
  62. public function getPermissions(bool $inherit = false)
  63. {
  64. if (null === $this->_permissionsCache) {
  65. $permissions = [];
  66. if ($inherit && $this->getNestedProperty('header.permissions.inherit', true)) {
  67. $parent = $this->parent();
  68. if ($parent && method_exists($parent, 'getPermissions')) {
  69. $permissions = $parent->getPermissions($inherit);
  70. }
  71. }
  72. $this->_permissionsCache = $this->loadPermissions($permissions);
  73. }
  74. return $this->_permissionsCache;
  75. }
  76. /**
  77. * @param iterable $authors
  78. * @return array<int,UserInterface>
  79. */
  80. protected function loadAuthors(iterable $authors): array
  81. {
  82. $accounts = $this->loadAccounts();
  83. if (null === $accounts || empty($authors)) {
  84. return [];
  85. }
  86. $list = [];
  87. foreach ($authors as $username) {
  88. if (!is_string($username)) {
  89. throw new InvalidArgumentException('Iterable should return username (string).', 500);
  90. }
  91. $list[] = $accounts->load($username);
  92. }
  93. return $list;
  94. }
  95. /**
  96. * @param string $action
  97. * @param string|null $scope
  98. * @param UserInterface|null $user
  99. * @param bool $isAuthor
  100. * @return bool|null
  101. */
  102. public function isParentAuthorized(string $action, string $scope = null, UserInterface $user = null, bool $isAuthor = false): ?bool
  103. {
  104. $scope = $scope ?? $this->getAuthorizeScope();
  105. $isMe = null === $user;
  106. if ($isMe) {
  107. $user = $this->getActiveUser();
  108. }
  109. if (null === $user) {
  110. return false;
  111. }
  112. return $this->isAuthorizedByGroup($user, $action, $scope, $isMe, $isAuthor);
  113. }
  114. /**
  115. * @param UserInterface $user
  116. * @param string $action
  117. * @param string $scope
  118. * @param bool $isMe
  119. * @return bool|null
  120. */
  121. protected function isAuthorizedOverride(UserInterface $user, string $action, string $scope, bool $isMe): ?bool
  122. {
  123. if ($action === 'delete' && $this->root()) {
  124. // Do not allow deleting root.
  125. return false;
  126. }
  127. $isAuthor = !$isMe || $user->authorized ? $this->hasAuthor($user->username) : false;
  128. return $this->isAuthorizedByGroup($user, $action, $scope, $isMe, $isAuthor) ?? parent::isAuthorizedOverride($user, $action, $scope, $isMe);
  129. }
  130. /**
  131. * Group authorization works as follows:
  132. *
  133. * 1. if any of the groups deny access, return false
  134. * 2. else if any of the groups allow access, return true
  135. * 3. else return null
  136. *
  137. * @param UserInterface $user
  138. * @param string $action
  139. * @param string $scope
  140. * @param bool $isMe
  141. * @param bool $isAuthor
  142. * @return bool|null
  143. */
  144. protected function isAuthorizedByGroup(UserInterface $user, string $action, string $scope, bool $isMe, bool $isAuthor): ?bool
  145. {
  146. $authorized = null;
  147. // In admin we want to check against group permissions.
  148. $pageGroups = $this->getPermissions();
  149. $userGroups = (array)$user->groups;
  150. /** @var Access $access */
  151. foreach ($pageGroups as $group => $access) {
  152. if ($group === 'defaults') {
  153. // Special defaults permissions group does not apply to guest.
  154. if ($isMe && !$user->authorized) {
  155. continue;
  156. }
  157. } elseif ($group === 'authors') {
  158. if (!$isAuthor) {
  159. continue;
  160. }
  161. } elseif (!in_array($group, $userGroups, true)) {
  162. continue;
  163. }
  164. $auth = $access->authorize($action);
  165. if (is_bool($auth)) {
  166. if ($auth === false) {
  167. return false;
  168. }
  169. $authorized = true;
  170. }
  171. }
  172. if (null === $authorized && $this->getNestedProperty('header.permissions.inherit', true)) {
  173. // Authorize against parent page.
  174. $parent = $this->parent();
  175. if ($parent && method_exists($parent, 'isParentAuthorized')) {
  176. $authorized = $parent->isParentAuthorized($action, $scope, !$isMe ? $user : null, $isAuthor);
  177. }
  178. }
  179. return $authorized;
  180. }
  181. /**
  182. * @param array $parent
  183. * @return array
  184. */
  185. protected function loadPermissions(array $parent = []): array
  186. {
  187. static $rules = [
  188. 'c' => 'create',
  189. 'r' => 'read',
  190. 'u' => 'update',
  191. 'd' => 'delete',
  192. 'p' => 'publish',
  193. 'l' => 'list'
  194. ];
  195. $permissions = $this->getNestedProperty('header.permissions.groups');
  196. $name = $this->root() ? '<root>' : '/' . $this->getKey();
  197. $list = [];
  198. if (is_array($permissions)) {
  199. foreach ($permissions as $group => $access) {
  200. $list[$group] = new Access($access, $rules, $name);
  201. }
  202. }
  203. foreach ($parent as $group => $access) {
  204. if (isset($list[$group])) {
  205. $object = $list[$group];
  206. } else {
  207. $object = new Access([], $rules, $name);
  208. $list[$group] = $object;
  209. }
  210. $object->inherit($access);
  211. }
  212. return $list;
  213. }
  214. }