diff options
Diffstat (limited to 'apps/workflowengine/lib/Check')
-rw-r--r-- | apps/workflowengine/lib/Check/AbstractStringCheck.php | 105 | ||||
-rw-r--r-- | apps/workflowengine/lib/Check/FileMimeType.php | 155 | ||||
-rw-r--r-- | apps/workflowengine/lib/Check/FileName.php | 75 | ||||
-rw-r--r-- | apps/workflowengine/lib/Check/FileSize.php | 99 | ||||
-rw-r--r-- | apps/workflowengine/lib/Check/FileSystemTags.php | 159 | ||||
-rw-r--r-- | apps/workflowengine/lib/Check/RequestRemoteAddress.php | 152 | ||||
-rw-r--r-- | apps/workflowengine/lib/Check/RequestTime.php | 114 | ||||
-rw-r--r-- | apps/workflowengine/lib/Check/RequestURL.php | 80 | ||||
-rw-r--r-- | apps/workflowengine/lib/Check/RequestUserAgent.php | 69 | ||||
-rw-r--r-- | apps/workflowengine/lib/Check/TFileCheck.php | 55 | ||||
-rw-r--r-- | apps/workflowengine/lib/Check/UserGroupMembership.php | 92 |
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; + } +} |