aboutsummaryrefslogtreecommitdiffstats
path: root/apps/workflowengine/lib/Check
diff options
context:
space:
mode:
Diffstat (limited to 'apps/workflowengine/lib/Check')
-rw-r--r--apps/workflowengine/lib/Check/AbstractStringCheck.php105
-rw-r--r--apps/workflowengine/lib/Check/FileMimeType.php155
-rw-r--r--apps/workflowengine/lib/Check/FileName.php75
-rw-r--r--apps/workflowengine/lib/Check/FileSize.php99
-rw-r--r--apps/workflowengine/lib/Check/FileSystemTags.php159
-rw-r--r--apps/workflowengine/lib/Check/RequestRemoteAddress.php152
-rw-r--r--apps/workflowengine/lib/Check/RequestTime.php114
-rw-r--r--apps/workflowengine/lib/Check/RequestURL.php80
-rw-r--r--apps/workflowengine/lib/Check/RequestUserAgent.php69
-rw-r--r--apps/workflowengine/lib/Check/TFileCheck.php55
-rw-r--r--apps/workflowengine/lib/Check/UserGroupMembership.php92
11 files changed, 1155 insertions, 0 deletions
diff --git a/apps/workflowengine/lib/Check/AbstractStringCheck.php b/apps/workflowengine/lib/Check/AbstractStringCheck.php
new file mode 100644
index 00000000000..d92e9901365
--- /dev/null
+++ b/apps/workflowengine/lib/Check/AbstractStringCheck.php
@@ -0,0 +1,105 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\WorkflowEngine\Check;
+
+use OCP\IL10N;
+use OCP\WorkflowEngine\ICheck;
+use OCP\WorkflowEngine\IManager;
+
+abstract class AbstractStringCheck implements ICheck {
+
+ /** @var array[] Nested array: [Pattern => [ActualValue => Regex Result]] */
+ protected $matches;
+
+ /**
+ * @param IL10N $l
+ */
+ public function __construct(
+ protected IL10N $l,
+ ) {
+ }
+
+ /**
+ * @return string
+ */
+ abstract protected function getActualValue();
+
+ /**
+ * @param string $operator
+ * @param string $value
+ * @return bool
+ */
+ public function executeCheck($operator, $value) {
+ $actualValue = $this->getActualValue();
+ return $this->executeStringCheck($operator, $value, $actualValue);
+ }
+
+ /**
+ * @param string $operator
+ * @param string $checkValue
+ * @param string $actualValue
+ * @return bool
+ */
+ protected function executeStringCheck($operator, $checkValue, $actualValue) {
+ if ($operator === 'is') {
+ return $checkValue === $actualValue;
+ } elseif ($operator === '!is') {
+ return $checkValue !== $actualValue;
+ } else {
+ $match = $this->match($checkValue, $actualValue);
+ if ($operator === 'matches') {
+ return $match === 1;
+ } else {
+ return $match === 0;
+ }
+ }
+ }
+
+ /**
+ * @param string $operator
+ * @param string $value
+ * @throws \UnexpectedValueException
+ */
+ public function validateCheck($operator, $value) {
+ if (!in_array($operator, ['is', '!is', 'matches', '!matches'])) {
+ throw new \UnexpectedValueException($this->l->t('The given operator is invalid'), 1);
+ }
+
+ if (in_array($operator, ['matches', '!matches'])
+ && @preg_match($value, null) === false) {
+ throw new \UnexpectedValueException($this->l->t('The given regular expression is invalid'), 2);
+ }
+ }
+
+ public function supportedEntities(): array {
+ // universal by default
+ return [];
+ }
+
+ public function isAvailableForScope(int $scope): bool {
+ // admin only by default
+ return $scope === IManager::SCOPE_ADMIN;
+ }
+
+ /**
+ * @param string $pattern
+ * @param string $subject
+ * @return int|bool
+ */
+ protected function match($pattern, $subject) {
+ $patternHash = md5($pattern);
+ $subjectHash = md5($subject);
+ if (isset($this->matches[$patternHash][$subjectHash])) {
+ return $this->matches[$patternHash][$subjectHash];
+ }
+ if (!isset($this->matches[$patternHash])) {
+ $this->matches[$patternHash] = [];
+ }
+ $this->matches[$patternHash][$subjectHash] = preg_match($pattern, $subject);
+ return $this->matches[$patternHash][$subjectHash];
+ }
+}
diff --git a/apps/workflowengine/lib/Check/FileMimeType.php b/apps/workflowengine/lib/Check/FileMimeType.php
new file mode 100644
index 00000000000..a8dfa64528e
--- /dev/null
+++ b/apps/workflowengine/lib/Check/FileMimeType.php
@@ -0,0 +1,155 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\WorkflowEngine\Check;
+
+use OC\Files\Storage\Local;
+use OCA\WorkflowEngine\Entity\File;
+use OCP\Files\IMimeTypeDetector;
+use OCP\Files\Storage\IStorage;
+use OCP\IL10N;
+use OCP\IRequest;
+use OCP\WorkflowEngine\IFileCheck;
+
+class FileMimeType extends AbstractStringCheck implements IFileCheck {
+ use TFileCheck {
+ setFileInfo as _setFileInfo;
+ }
+
+ /** @var array */
+ protected $mimeType;
+
+ /**
+ * @param IL10N $l
+ * @param IRequest $request
+ * @param IMimeTypeDetector $mimeTypeDetector
+ */
+ public function __construct(
+ IL10N $l,
+ protected IRequest $request,
+ protected IMimeTypeDetector $mimeTypeDetector,
+ ) {
+ parent::__construct($l);
+ }
+
+ /**
+ * @param IStorage $storage
+ * @param string $path
+ * @param bool $isDir
+ */
+ public function setFileInfo(IStorage $storage, string $path, bool $isDir = false): void {
+ $this->_setFileInfo($storage, $path, $isDir);
+ if (!isset($this->mimeType[$this->storage->getId()][$this->path])
+ || $this->mimeType[$this->storage->getId()][$this->path] === '') {
+ if ($isDir) {
+ $this->mimeType[$this->storage->getId()][$this->path] = 'httpd/unix-directory';
+ } else {
+ $this->mimeType[$this->storage->getId()][$this->path] = null;
+ }
+ }
+ }
+
+ /**
+ * The mimetype is only cached if the file has a valid mimetype. Otherwise files access
+ * control will cache "application/octet-stream" for all the target node on:
+ * rename, move, copy and all other methods which create a new item
+ *
+ * To check this:
+ * 1. Add an automated tagging rule which tags on mimetype NOT "httpd/unix-directory"
+ * 2. Add an access control rule which checks for any mimetype
+ * 3. Create a folder and rename it, the folder should not be tagged, but it is
+ *
+ * @param string $storageId
+ * @param string|null $path
+ * @param string $mimeType
+ * @return string
+ */
+ protected function cacheAndReturnMimeType(string $storageId, ?string $path, string $mimeType): string {
+ if ($path !== null && $mimeType !== 'application/octet-stream') {
+ $this->mimeType[$storageId][$path] = $mimeType;
+ }
+
+ return $mimeType;
+ }
+
+ /**
+ * Make sure that even though the content based check returns an application/octet-stream can still be checked based on mimetypemappings of their extension
+ *
+ * @param string $operator
+ * @param string $value
+ * @return bool
+ */
+ public function executeCheck($operator, $value) {
+ return $this->executeStringCheck($operator, $value, $this->getActualValue());
+ }
+
+ /**
+ * @return string
+ */
+ protected function getActualValue() {
+ if ($this->mimeType[$this->storage->getId()][$this->path] !== null) {
+ return $this->mimeType[$this->storage->getId()][$this->path];
+ }
+ $cacheEntry = $this->storage->getCache()->get($this->path);
+ if ($cacheEntry && $cacheEntry->getMimeType() !== 'application/octet-stream') {
+ return $this->cacheAndReturnMimeType($this->storage->getId(), $this->path, $cacheEntry->getMimeType());
+ }
+
+ if ($this->storage->file_exists($this->path)
+ && $this->storage->filesize($this->path)
+ && $this->storage->instanceOfStorage(Local::class)
+ ) {
+ $path = $this->storage->getLocalFile($this->path);
+ $mimeType = $this->mimeTypeDetector->detectContent($path);
+ return $this->cacheAndReturnMimeType($this->storage->getId(), $this->path, $mimeType);
+ }
+
+ if ($this->isWebDAVRequest() || $this->isPublicWebDAVRequest()) {
+ // Creating a folder
+ if ($this->request->getMethod() === 'MKCOL') {
+ return 'httpd/unix-directory';
+ }
+ }
+
+ // We do not cache this, as the file did not exist yet.
+ // In case it does in the future, we will check with detectContent()
+ // again to get the real mimetype of the content, rather than
+ // guessing it from the path.
+ return $this->mimeTypeDetector->detectPath($this->path);
+ }
+
+ /**
+ * @return bool
+ */
+ protected function isWebDAVRequest() {
+ return substr($this->request->getScriptName(), 0 - strlen('/remote.php')) === '/remote.php' && (
+ $this->request->getPathInfo() === '/webdav'
+ || str_starts_with($this->request->getPathInfo() ?? '', '/webdav/')
+ || $this->request->getPathInfo() === '/dav/files'
+ || str_starts_with($this->request->getPathInfo() ?? '', '/dav/files/')
+ || $this->request->getPathInfo() === '/dav/uploads'
+ || str_starts_with($this->request->getPathInfo() ?? '', '/dav/uploads/')
+ );
+ }
+
+ /**
+ * @return bool
+ */
+ protected function isPublicWebDAVRequest() {
+ return substr($this->request->getScriptName(), 0 - strlen('/public.php')) === '/public.php' && (
+ $this->request->getPathInfo() === '/webdav'
+ || str_starts_with($this->request->getPathInfo() ?? '', '/webdav/')
+ );
+ }
+
+ public function supportedEntities(): array {
+ return [ File::class ];
+ }
+
+ public function isAvailableForScope(int $scope): bool {
+ return true;
+ }
+}
diff --git a/apps/workflowengine/lib/Check/FileName.php b/apps/workflowengine/lib/Check/FileName.php
new file mode 100644
index 00000000000..4a9d503018f
--- /dev/null
+++ b/apps/workflowengine/lib/Check/FileName.php
@@ -0,0 +1,75 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\WorkflowEngine\Check;
+
+use OC\Files\Storage\Local;
+use OCA\WorkflowEngine\Entity\File;
+use OCP\Files\Mount\IMountManager;
+use OCP\IL10N;
+use OCP\IRequest;
+use OCP\WorkflowEngine\IFileCheck;
+
+class FileName extends AbstractStringCheck implements IFileCheck {
+ use TFileCheck;
+
+ /**
+ * @param IL10N $l
+ * @param IRequest $request
+ */
+ public function __construct(
+ IL10N $l,
+ protected IRequest $request,
+ private IMountManager $mountManager,
+ ) {
+ parent::__construct($l);
+ }
+
+ /**
+ * @return string
+ */
+ protected function getActualValue(): string {
+ $fileName = $this->path === null ? '' : basename($this->path);
+ if ($fileName === '' && (!$this->storage->isLocal() || $this->storage->instanceOfStorage(Local::class))) {
+ // Return the mountpoint name of external storage that are not mounted as user home
+ $mountPoints = $this->mountManager->findByStorageId($this->storage->getId());
+ if (empty($mountPoints) || $mountPoints[0]->getMountType() !== 'external') {
+ return $fileName;
+ }
+ $mountPointPath = rtrim($mountPoints[0]->getMountPoint(), '/');
+ $mountPointPieces = explode('/', $mountPointPath);
+ $mountPointName = array_pop($mountPointPieces);
+ if (!empty($mountPointName) && $mountPointName !== 'files' && count($mountPointPieces) !== 2) {
+ return $mountPointName;
+ }
+ }
+ return $fileName;
+ }
+
+ /**
+ * @param string $operator
+ * @param string $checkValue
+ * @param string $actualValue
+ * @return bool
+ */
+ protected function executeStringCheck($operator, $checkValue, $actualValue): bool {
+ if ($operator === 'is' || $operator === '!is') {
+ $checkValue = mb_strtolower($checkValue);
+ $actualValue = mb_strtolower($actualValue);
+ }
+ return parent::executeStringCheck($operator, $checkValue, $actualValue);
+ }
+
+ public function supportedEntities(): array {
+ return [ File::class ];
+ }
+
+ public function isAvailableForScope(int $scope): bool {
+ return true;
+ }
+}
diff --git a/apps/workflowengine/lib/Check/FileSize.php b/apps/workflowengine/lib/Check/FileSize.php
new file mode 100644
index 00000000000..5ee03ccc9cf
--- /dev/null
+++ b/apps/workflowengine/lib/Check/FileSize.php
@@ -0,0 +1,99 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\WorkflowEngine\Check;
+
+use OCA\WorkflowEngine\Entity\File;
+use OCP\IL10N;
+use OCP\IRequest;
+use OCP\Util;
+use OCP\WorkflowEngine\ICheck;
+
+class FileSize implements ICheck {
+
+ /** @var int */
+ protected $size;
+
+ /**
+ * @param IL10N $l
+ * @param IRequest $request
+ */
+ public function __construct(
+ protected IL10N $l,
+ protected IRequest $request,
+ ) {
+ }
+
+ /**
+ * @param string $operator
+ * @param string $value
+ * @return bool
+ */
+ public function executeCheck($operator, $value) {
+ $size = $this->getFileSizeFromHeader();
+
+ $value = Util::computerFileSize($value);
+ if ($size !== false) {
+ switch ($operator) {
+ case 'less':
+ return $size < $value;
+ case '!less':
+ return $size >= $value;
+ case 'greater':
+ return $size > $value;
+ case '!greater':
+ return $size <= $value;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * @param string $operator
+ * @param string $value
+ * @throws \UnexpectedValueException
+ */
+ public function validateCheck($operator, $value) {
+ if (!in_array($operator, ['less', '!less', 'greater', '!greater'])) {
+ throw new \UnexpectedValueException($this->l->t('The given operator is invalid'), 1);
+ }
+
+ if (!preg_match('/^[0-9]+[ ]?[kmgt]?b$/i', $value)) {
+ throw new \UnexpectedValueException($this->l->t('The given file size is invalid'), 2);
+ }
+ }
+
+ /**
+ * @return string
+ */
+ protected function getFileSizeFromHeader() {
+ if ($this->size !== null) {
+ return $this->size;
+ }
+
+ $size = $this->request->getHeader('OC-Total-Length');
+ if ($size === '') {
+ if (in_array($this->request->getMethod(), ['POST', 'PUT'])) {
+ $size = $this->request->getHeader('Content-Length');
+ }
+ }
+
+ if ($size === '') {
+ $size = false;
+ }
+
+ $this->size = $size;
+ return $this->size;
+ }
+
+ public function supportedEntities(): array {
+ return [ File::class ];
+ }
+
+ public function isAvailableForScope(int $scope): bool {
+ return true;
+ }
+}
diff --git a/apps/workflowengine/lib/Check/FileSystemTags.php b/apps/workflowengine/lib/Check/FileSystemTags.php
new file mode 100644
index 00000000000..811571f558a
--- /dev/null
+++ b/apps/workflowengine/lib/Check/FileSystemTags.php
@@ -0,0 +1,159 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\WorkflowEngine\Check;
+
+use OC\Files\Storage\Wrapper\Jail;
+use OCA\Files_Sharing\SharedStorage;
+use OCA\WorkflowEngine\Entity\File;
+use OCP\Files\Cache\ICache;
+use OCP\Files\IHomeStorage;
+use OCP\IGroupManager;
+use OCP\IL10N;
+use OCP\IUser;
+use OCP\IUserSession;
+use OCP\SystemTag\ISystemTagManager;
+use OCP\SystemTag\ISystemTagObjectMapper;
+use OCP\SystemTag\TagNotFoundException;
+use OCP\WorkflowEngine\ICheck;
+use OCP\WorkflowEngine\IFileCheck;
+
+class FileSystemTags implements ICheck, IFileCheck {
+ use TFileCheck;
+
+ /** @var array */
+ protected $fileIds;
+
+ /** @var array */
+ protected $fileSystemTags;
+
+ public function __construct(
+ protected IL10N $l,
+ protected ISystemTagManager $systemTagManager,
+ protected ISystemTagObjectMapper $systemTagObjectMapper,
+ protected IUserSession $userSession,
+ protected IGroupManager $groupManager,
+ ) {
+ }
+
+ /**
+ * @param string $operator
+ * @param string $value
+ * @return bool
+ */
+ public function executeCheck($operator, $value) {
+ $systemTags = $this->getSystemTags();
+ return ($operator === 'is') === in_array($value, $systemTags);
+ }
+
+ /**
+ * @param string $operator
+ * @param string $value
+ * @throws \UnexpectedValueException
+ */
+ public function validateCheck($operator, $value) {
+ if (!in_array($operator, ['is', '!is'])) {
+ throw new \UnexpectedValueException($this->l->t('The given operator is invalid'), 1);
+ }
+
+ try {
+ $tags = $this->systemTagManager->getTagsByIds($value);
+
+ $user = $this->userSession->getUser();
+ $isAdmin = $user instanceof IUser && $this->groupManager->isAdmin($user->getUID());
+
+ if (!$isAdmin) {
+ foreach ($tags as $tag) {
+ if (!$tag->isUserVisible()) {
+ throw new \UnexpectedValueException($this->l->t('The given tag id is invalid'), 4);
+ }
+ }
+ }
+ } catch (TagNotFoundException $e) {
+ throw new \UnexpectedValueException($this->l->t('The given tag id is invalid'), 2);
+ } catch (\InvalidArgumentException $e) {
+ throw new \UnexpectedValueException($this->l->t('The given tag id is invalid'), 3);
+ }
+ }
+
+ /**
+ * Get the ids of the assigned system tags
+ * @return string[]
+ */
+ protected function getSystemTags() {
+ $cache = $this->storage->getCache();
+ $fileIds = $this->getFileIds($cache, $this->path, !$this->storage->instanceOfStorage(IHomeStorage::class) || $this->storage->instanceOfStorage(SharedStorage::class));
+
+ $systemTags = [];
+ foreach ($fileIds as $i => $fileId) {
+ if (isset($this->fileSystemTags[$fileId])) {
+ $systemTags[] = $this->fileSystemTags[$fileId];
+ unset($fileIds[$i]);
+ }
+ }
+
+ if (!empty($fileIds)) {
+ $mappedSystemTags = $this->systemTagObjectMapper->getTagIdsForObjects($fileIds, 'files');
+ foreach ($mappedSystemTags as $fileId => $fileSystemTags) {
+ $this->fileSystemTags[$fileId] = $fileSystemTags;
+ $systemTags[] = $fileSystemTags;
+ }
+ }
+
+ $systemTags = call_user_func_array('array_merge', $systemTags);
+ $systemTags = array_unique($systemTags);
+ return $systemTags;
+ }
+
+ /**
+ * Get the file ids of the given path and its parents
+ * @param ICache $cache
+ * @param string $path
+ * @param bool $isExternalStorage
+ * @return int[]
+ */
+ protected function getFileIds(ICache $cache, $path, $isExternalStorage) {
+ $cacheId = $cache->getNumericStorageId();
+ if ($this->storage->instanceOfStorage(Jail::class)) {
+ $absolutePath = $this->storage->getUnjailedPath($path);
+ } else {
+ $absolutePath = $path;
+ }
+
+ if (isset($this->fileIds[$cacheId][$absolutePath])) {
+ return $this->fileIds[$cacheId][$absolutePath];
+ }
+
+ $parentIds = [];
+ if ($path !== $this->dirname($path)) {
+ $parentIds = $this->getFileIds($cache, $this->dirname($path), $isExternalStorage);
+ } elseif (!$isExternalStorage) {
+ return [];
+ }
+
+ $fileId = $cache->getId($path);
+ if ($fileId !== -1) {
+ $parentIds[] = $fileId;
+ }
+
+ $this->fileIds[$cacheId][$absolutePath] = $parentIds;
+
+ return $parentIds;
+ }
+
+ protected function dirname($path) {
+ $dir = dirname($path);
+ return $dir === '.' ? '' : $dir;
+ }
+
+ public function supportedEntities(): array {
+ return [ File::class ];
+ }
+
+ public function isAvailableForScope(int $scope): bool {
+ return true;
+ }
+}
diff --git a/apps/workflowengine/lib/Check/RequestRemoteAddress.php b/apps/workflowengine/lib/Check/RequestRemoteAddress.php
new file mode 100644
index 00000000000..b6f8fef5aed
--- /dev/null
+++ b/apps/workflowengine/lib/Check/RequestRemoteAddress.php
@@ -0,0 +1,152 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\WorkflowEngine\Check;
+
+use OCP\IL10N;
+use OCP\IRequest;
+use OCP\WorkflowEngine\ICheck;
+
+class RequestRemoteAddress implements ICheck {
+
+ /**
+ * @param IL10N $l
+ * @param IRequest $request
+ */
+ public function __construct(
+ protected IL10N $l,
+ protected IRequest $request,
+ ) {
+ }
+
+ /**
+ * @param string $operator
+ * @param string $value
+ * @return bool
+ */
+ public function executeCheck($operator, $value) {
+ $actualValue = $this->request->getRemoteAddress();
+ $decodedValue = explode('/', $value);
+
+ if ($operator === 'matchesIPv4') {
+ return $this->matchIPv4($actualValue, $decodedValue[0], $decodedValue[1]);
+ } elseif ($operator === '!matchesIPv4') {
+ return !$this->matchIPv4($actualValue, $decodedValue[0], $decodedValue[1]);
+ } elseif ($operator === 'matchesIPv6') {
+ return $this->matchIPv6($actualValue, $decodedValue[0], $decodedValue[1]);
+ } else {
+ return !$this->matchIPv6($actualValue, $decodedValue[0], $decodedValue[1]);
+ }
+ }
+
+ /**
+ * @param string $operator
+ * @param string $value
+ * @throws \UnexpectedValueException
+ */
+ public function validateCheck($operator, $value) {
+ if (!in_array($operator, ['matchesIPv4', '!matchesIPv4', 'matchesIPv6', '!matchesIPv6'])) {
+ throw new \UnexpectedValueException($this->l->t('The given operator is invalid'), 1);
+ }
+
+ $decodedValue = explode('/', $value);
+ if (count($decodedValue) !== 2) {
+ throw new \UnexpectedValueException($this->l->t('The given IP range is invalid'), 2);
+ }
+
+ if (in_array($operator, ['matchesIPv4', '!matchesIPv4'])) {
+ if (!filter_var($decodedValue[0], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
+ throw new \UnexpectedValueException($this->l->t('The given IP range is not valid for IPv4'), 3);
+ }
+ if ($decodedValue[1] > 32 || $decodedValue[1] <= 0) {
+ throw new \UnexpectedValueException($this->l->t('The given IP range is not valid for IPv4'), 4);
+ }
+ } else {
+ if (!filter_var($decodedValue[0], FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
+ throw new \UnexpectedValueException($this->l->t('The given IP range is not valid for IPv6'), 3);
+ }
+ if ($decodedValue[1] > 128 || $decodedValue[1] <= 0) {
+ throw new \UnexpectedValueException($this->l->t('The given IP range is not valid for IPv6'), 4);
+ }
+ }
+ }
+
+ /**
+ * Based on https://stackoverflow.com/a/594134
+ * @param string $ip
+ * @param string $rangeIp
+ * @param int $bits
+ * @return bool
+ */
+ protected function matchIPv4($ip, $rangeIp, $bits) {
+ $rangeDecimal = ip2long($rangeIp);
+ $ipDecimal = ip2long($ip);
+ $mask = -1 << (32 - $bits);
+ return ($ipDecimal & $mask) === ($rangeDecimal & $mask);
+ }
+
+ /**
+ * Based on https://stackoverflow.com/a/7951507
+ * @param string $ip
+ * @param string $rangeIp
+ * @param int $bits
+ * @return bool
+ */
+ protected function matchIPv6($ip, $rangeIp, $bits) {
+ $ipNet = inet_pton($ip);
+ $binaryIp = $this->ipv6ToBits($ipNet);
+ $ipNetBits = substr($binaryIp, 0, $bits);
+
+ $rangeNet = inet_pton($rangeIp);
+ $binaryRange = $this->ipv6ToBits($rangeNet);
+ $rangeNetBits = substr($binaryRange, 0, $bits);
+
+ return $ipNetBits === $rangeNetBits;
+ }
+
+ /**
+ * Based on https://stackoverflow.com/a/7951507
+ * @param string $packedIp
+ * @return string
+ */
+ protected function ipv6ToBits($packedIp) {
+ $unpackedIp = unpack('A16', $packedIp);
+ $unpackedIp = str_split($unpackedIp[1]);
+ $binaryIp = '';
+ foreach ($unpackedIp as $char) {
+ $binaryIp .= str_pad(decbin(ord($char)), 8, '0', STR_PAD_LEFT);
+ }
+ return str_pad($binaryIp, 128, '0', STR_PAD_RIGHT);
+ }
+
+ /**
+ * returns a list of Entities the checker supports. The values must match
+ * the class name of the entity.
+ *
+ * An empty result means the check is universally available.
+ *
+ * @since 18.0.0
+ */
+ public function supportedEntities(): array {
+ return [];
+ }
+
+ /**
+ * returns whether the operation can be used in the requested scope.
+ *
+ * Scope IDs are defined as constants in OCP\WorkflowEngine\IManager. At
+ * time of writing these are SCOPE_ADMIN and SCOPE_USER.
+ *
+ * For possibly unknown future scopes the recommended behaviour is: if
+ * user scope is permitted, the default behaviour should return `true`,
+ * otherwise `false`.
+ *
+ * @since 18.0.0
+ */
+ public function isAvailableForScope(int $scope): bool {
+ return true;
+ }
+}
diff --git a/apps/workflowengine/lib/Check/RequestTime.php b/apps/workflowengine/lib/Check/RequestTime.php
new file mode 100644
index 00000000000..a49986652b8
--- /dev/null
+++ b/apps/workflowengine/lib/Check/RequestTime.php
@@ -0,0 +1,114 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\WorkflowEngine\Check;
+
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\IL10N;
+use OCP\WorkflowEngine\ICheck;
+
+class RequestTime implements ICheck {
+ public const REGEX_TIME = '([0-1][0-9]|2[0-3]):([0-5][0-9])';
+ public const REGEX_TIMEZONE = '([a-zA-Z]+(?:\\/[a-zA-Z\-\_]+)+)';
+
+ /** @var bool[] */
+ protected $cachedResults;
+
+ /**
+ * @param ITimeFactory $timeFactory
+ */
+ public function __construct(
+ protected IL10N $l,
+ protected ITimeFactory $timeFactory,
+ ) {
+ }
+
+ /**
+ * @param string $operator
+ * @param string $value
+ * @return bool
+ */
+ public function executeCheck($operator, $value) {
+ $valueHash = md5($value);
+
+ if (isset($this->cachedResults[$valueHash])) {
+ return $this->cachedResults[$valueHash];
+ }
+
+ $timestamp = $this->timeFactory->getTime();
+
+ $values = json_decode($value, true);
+ $timestamp1 = $this->getTimestamp($timestamp, $values[0]);
+ $timestamp2 = $this->getTimestamp($timestamp, $values[1]);
+
+ if ($timestamp1 < $timestamp2) {
+ $in = $timestamp1 <= $timestamp && $timestamp <= $timestamp2;
+ } else {
+ $in = $timestamp1 <= $timestamp || $timestamp <= $timestamp2;
+ }
+
+ return ($operator === 'in') ? $in : !$in;
+ }
+
+ /**
+ * @param int $currentTimestamp
+ * @param string $value Format: "H:i e"
+ * @return int
+ */
+ protected function getTimestamp($currentTimestamp, $value) {
+ [$time1, $timezone1] = explode(' ', $value);
+ [$hour1, $minute1] = explode(':', $time1);
+ $date1 = new \DateTime('now', new \DateTimeZone($timezone1));
+ $date1->setTimestamp($currentTimestamp);
+ $date1->setTime((int)$hour1, (int)$minute1);
+
+ return $date1->getTimestamp();
+ }
+
+ /**
+ * @param string $operator
+ * @param string $value
+ * @throws \UnexpectedValueException
+ */
+ public function validateCheck($operator, $value) {
+ if (!in_array($operator, ['in', '!in'])) {
+ throw new \UnexpectedValueException($this->l->t('The given operator is invalid'), 1);
+ }
+
+ $regexValue = '\"' . self::REGEX_TIME . ' ' . self::REGEX_TIMEZONE . '\"';
+ $result = preg_match('/^\[' . $regexValue . ',' . $regexValue . '\]$/', $value, $matches);
+ if (!$result) {
+ throw new \UnexpectedValueException($this->l->t('The given time span is invalid'), 2);
+ }
+
+ $values = json_decode($value, true);
+ $time1 = \DateTime::createFromFormat('H:i e', (string)$values[0]);
+ if ($time1 === false) {
+ throw new \UnexpectedValueException($this->l->t('The given start time is invalid'), 3);
+ }
+
+ $time2 = \DateTime::createFromFormat('H:i e', (string)$values[1]);
+ if ($time2 === false) {
+ throw new \UnexpectedValueException($this->l->t('The given end time is invalid'), 4);
+ }
+ }
+
+ public function isAvailableForScope(int $scope): bool {
+ return true;
+ }
+
+ /**
+ * returns a list of Entities the checker supports. The values must match
+ * the class name of the entity.
+ *
+ * An empty result means the check is universally available.
+ *
+ * @since 18.0.0
+ */
+ public function supportedEntities(): array {
+ return [];
+ }
+}
diff --git a/apps/workflowengine/lib/Check/RequestURL.php b/apps/workflowengine/lib/Check/RequestURL.php
new file mode 100644
index 00000000000..fb2ac7e8fd5
--- /dev/null
+++ b/apps/workflowengine/lib/Check/RequestURL.php
@@ -0,0 +1,80 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\WorkflowEngine\Check;
+
+use OCP\IL10N;
+use OCP\IRequest;
+
+class RequestURL extends AbstractStringCheck {
+ public const CLI = 'cli';
+
+ /** @var ?string */
+ protected $url;
+
+ /**
+ * @param IL10N $l
+ * @param IRequest $request
+ */
+ public function __construct(
+ IL10N $l,
+ protected IRequest $request,
+ ) {
+ parent::__construct($l);
+ }
+
+ /**
+ * @param string $operator
+ * @param string $value
+ * @return bool
+ */
+ public function executeCheck($operator, $value) {
+ if (\OC::$CLI) {
+ $actualValue = $this->url = RequestURL::CLI;
+ } else {
+ $actualValue = $this->getActualValue();
+ }
+ if (in_array($operator, ['is', '!is'])) {
+ switch ($value) {
+ case 'webdav':
+ if ($operator === 'is') {
+ return $this->isWebDAVRequest();
+ } else {
+ return !$this->isWebDAVRequest();
+ }
+ }
+ }
+ return $this->executeStringCheck($operator, $value, $actualValue);
+ }
+
+ /**
+ * @return string
+ */
+ protected function getActualValue() {
+ if ($this->url !== null) {
+ return $this->url;
+ }
+
+ $this->url = $this->request->getServerProtocol() . '://';// E.g. http(s) + ://
+ $this->url .= $this->request->getServerHost();// E.g. localhost
+ $this->url .= $this->request->getScriptName();// E.g. /nextcloud/index.php
+ $this->url .= $this->request->getPathInfo();// E.g. /apps/files_texteditor/ajax/loadfile
+
+ return $this->url; // E.g. https://localhost/nextcloud/index.php/apps/files_texteditor/ajax/loadfile
+ }
+
+ protected function isWebDAVRequest(): bool {
+ if ($this->url === RequestURL::CLI) {
+ return false;
+ }
+ return substr($this->request->getScriptName(), 0 - strlen('/remote.php')) === '/remote.php' && (
+ $this->request->getPathInfo() === '/webdav'
+ || str_starts_with($this->request->getPathInfo() ?? '', '/webdav/')
+ || $this->request->getPathInfo() === '/dav/files'
+ || str_starts_with($this->request->getPathInfo() ?? '', '/dav/files/')
+ );
+ }
+}
diff --git a/apps/workflowengine/lib/Check/RequestUserAgent.php b/apps/workflowengine/lib/Check/RequestUserAgent.php
new file mode 100644
index 00000000000..572ef567074
--- /dev/null
+++ b/apps/workflowengine/lib/Check/RequestUserAgent.php
@@ -0,0 +1,69 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\WorkflowEngine\Check;
+
+use OCP\IL10N;
+use OCP\IRequest;
+
+class RequestUserAgent extends AbstractStringCheck {
+
+ /**
+ * @param IL10N $l
+ * @param IRequest $request
+ */
+ public function __construct(
+ IL10N $l,
+ protected IRequest $request,
+ ) {
+ parent::__construct($l);
+ }
+
+ /**
+ * @param string $operator
+ * @param string $value
+ * @return bool
+ */
+ public function executeCheck($operator, $value) {
+ $actualValue = $this->getActualValue();
+ if (in_array($operator, ['is', '!is'], true)) {
+ switch ($value) {
+ case 'android':
+ $operator = $operator === 'is' ? 'matches' : '!matches';
+ $value = IRequest::USER_AGENT_CLIENT_ANDROID;
+ break;
+ case 'ios':
+ $operator = $operator === 'is' ? 'matches' : '!matches';
+ $value = IRequest::USER_AGENT_CLIENT_IOS;
+ break;
+ case 'desktop':
+ $operator = $operator === 'is' ? 'matches' : '!matches';
+ $value = IRequest::USER_AGENT_CLIENT_DESKTOP;
+ break;
+ case 'mail':
+ if ($operator === 'is') {
+ return $this->executeStringCheck('matches', IRequest::USER_AGENT_OUTLOOK_ADDON, $actualValue)
+ || $this->executeStringCheck('matches', IRequest::USER_AGENT_THUNDERBIRD_ADDON, $actualValue);
+ }
+
+ return $this->executeStringCheck('!matches', IRequest::USER_AGENT_OUTLOOK_ADDON, $actualValue)
+ && $this->executeStringCheck('!matches', IRequest::USER_AGENT_THUNDERBIRD_ADDON, $actualValue);
+ }
+ }
+ return $this->executeStringCheck($operator, $value, $actualValue);
+ }
+
+ /**
+ * @return string
+ */
+ protected function getActualValue() {
+ return $this->request->getHeader('User-Agent');
+ }
+
+ public function isAvailableForScope(int $scope): bool {
+ return true;
+ }
+}
diff --git a/apps/workflowengine/lib/Check/TFileCheck.php b/apps/workflowengine/lib/Check/TFileCheck.php
new file mode 100644
index 00000000000..a514352e047
--- /dev/null
+++ b/apps/workflowengine/lib/Check/TFileCheck.php
@@ -0,0 +1,55 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\WorkflowEngine\Check;
+
+use OCA\WorkflowEngine\AppInfo\Application;
+use OCA\WorkflowEngine\Entity\File;
+use OCP\Files\Node;
+use OCP\Files\NotFoundException;
+use OCP\Files\Storage\IStorage;
+use OCP\WorkflowEngine\IEntity;
+
+trait TFileCheck {
+ /** @var IStorage */
+ protected $storage;
+
+ /** @var string */
+ protected $path;
+
+ /** @var bool */
+ protected $isDir;
+
+ /**
+ * @param IStorage $storage
+ * @param string $path
+ * @param bool $isDir
+ * @since 18.0.0
+ */
+ public function setFileInfo(IStorage $storage, string $path, bool $isDir = false): void {
+ $this->storage = $storage;
+ $this->path = $path;
+ $this->isDir = $isDir;
+ }
+
+ /**
+ * @throws NotFoundException
+ */
+ public function setEntitySubject(IEntity $entity, $subject): void {
+ if ($entity instanceof File) {
+ if (!$subject instanceof Node) {
+ throw new \UnexpectedValueException(
+ 'Expected Node subject for File entity, got {class}',
+ ['app' => Application::APP_ID, 'class' => get_class($subject)]
+ );
+ }
+ $this->storage = $subject->getStorage();
+ $this->path = $subject->getPath();
+ }
+ }
+}
diff --git a/apps/workflowengine/lib/Check/UserGroupMembership.php b/apps/workflowengine/lib/Check/UserGroupMembership.php
new file mode 100644
index 00000000000..690f9974a49
--- /dev/null
+++ b/apps/workflowengine/lib/Check/UserGroupMembership.php
@@ -0,0 +1,92 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\WorkflowEngine\Check;
+
+use OCP\IGroupManager;
+use OCP\IL10N;
+use OCP\IUser;
+use OCP\IUserSession;
+use OCP\WorkflowEngine\ICheck;
+use OCP\WorkflowEngine\IManager;
+
+class UserGroupMembership implements ICheck {
+
+ /** @var string */
+ protected $cachedUser;
+
+ /** @var string[] */
+ protected $cachedGroupMemberships;
+
+ /**
+ * @param IUserSession $userSession
+ * @param IGroupManager $groupManager
+ * @param IL10N $l
+ */
+ public function __construct(
+ protected IUserSession $userSession,
+ protected IGroupManager $groupManager,
+ protected IL10N $l,
+ ) {
+ }
+
+ /**
+ * @param string $operator
+ * @param string $value
+ * @return bool
+ */
+ public function executeCheck($operator, $value) {
+ $user = $this->userSession->getUser();
+
+ if ($user instanceof IUser) {
+ $groupIds = $this->getUserGroups($user);
+ return ($operator === 'is') === in_array($value, $groupIds);
+ } else {
+ return $operator !== 'is';
+ }
+ }
+
+
+ /**
+ * @param string $operator
+ * @param string $value
+ * @throws \UnexpectedValueException
+ */
+ public function validateCheck($operator, $value) {
+ if (!in_array($operator, ['is', '!is'])) {
+ throw new \UnexpectedValueException($this->l->t('The given operator is invalid'), 1);
+ }
+
+ if (!$this->groupManager->groupExists($value)) {
+ throw new \UnexpectedValueException($this->l->t('The given group does not exist'), 2);
+ }
+ }
+
+ /**
+ * @param IUser $user
+ * @return string[]
+ */
+ protected function getUserGroups(IUser $user) {
+ $uid = $user->getUID();
+
+ if ($this->cachedUser !== $uid) {
+ $this->cachedUser = $uid;
+ $this->cachedGroupMemberships = $this->groupManager->getUserGroupIds($user);
+ }
+
+ return $this->cachedGroupMemberships;
+ }
+
+ public function supportedEntities(): array {
+ // universal by default
+ return [];
+ }
+
+ public function isAvailableForScope(int $scope): bool {
+ // admin only by default
+ return $scope === IManager::SCOPE_ADMIN;
+ }
+}