<?php
/*
* Copyright (C) Ascensio System SIA, 2009-2026
*
* This program is a free software product. You can redistribute it and/or
* modify it under the terms of the GNU Affero General Public License (AGPL)
* version 3 as published by the Free Software Foundation, together with the
* additional terms provided in the LICENSE file.
*
* This program is distributed WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For
* details, see the GNU AGPL at: https://www.gnu.org/licenses/agpl-3.0.html
*
* You can contact Ascensio System SIA by email at info@onlyoffice.com
* or by postal mail at 20A-6 Ernesta Birznieka-Upisha Street, Riga,
* LV-1050, Latvia, European Union.
*
* The interactive user interfaces in modified versions of the Program
* are required to display Appropriate Legal Notices in accordance with
* Section 5 of the GNU AGPL version 3.
*
* No trademark rights are granted under this License.
*
* All non-code elements of the Product, including illustrations,
* icon sets, and technical writing content, are licensed under the
* Creative Commons Attribution-ShareAlike 4.0 International License:
* https://creativecommons.org/licenses/by-sa/4.0/legalcode
*
* This license applies only to such non-code elements and does not
* modify or replace the licensing terms applicable to the Program's
* source code, which remains licensed under the GNU Affero General
* Public License v3.
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\Onlyoffice;
use OCA\Talk\Manager as TalkManager;
use OCP\Constants;
use OCP\IAppConfig;
use OCP\IDBConnection;
use OCP\Share\Exceptions\ShareNotFound;
use OCP\Share\IManager;
use OCP\Share\IShare;
use Psr\Log\LoggerInterface;
/**
* Class expands base permissions
*
* @package OCA\Onlyoffice
*/
class ExtraPermissions {
/**
* Table name
*/
private const TABLENAME_KEY = "onlyoffice_permissions";
/**
* Extra permission values
*
* @var integer
*/
public const NONE = 0;
public const REVIEW = 1;
public const COMMENT = 2;
public const FILLFORMS = 4;
public const MODIFYFILTER = 8;
public function __construct(
private readonly LoggerInterface $logger,
private readonly IManager $shareManager,
private readonly IAppConfig $config,
private readonly AppConfig $appConfig,
private readonly IDBConnection $connection,
private readonly ?TalkManager $talkManager,
) {}
/**
* Get extra permissions by shareId
*/
public function getExtra(string $shareId): ?array {
$share = $this->getShare($shareId);
if (!$share instanceof IShare) {
return null;
}
$shareId = $share->getId();
$extra = $this->get($shareId);
$wasInit = isset($extra["permissions"]);
$checkExtra = $wasInit ? (int)$extra["permissions"] : self::NONE;
[$availableExtra, $defaultPermissions] = $this->validation($share, $checkExtra, $wasInit);
if ($availableExtra === 0
|| ($availableExtra & $checkExtra) !== $checkExtra) {
if (!empty($extra)) {
$this->delete($shareId);
}
$this->logger->debug("Share " . $shareId . " does not support extra permissions");
return null;
}
if (empty($extra)) {
$extra["id"] = -1;
$extra["share_id"] = $share->getId();
$extra["permissions"] = $defaultPermissions;
}
$extra["type"] = $share->getShareType();
$extra["shareWith"] = $share->getSharedWith();
$extra["shareWithName"] = $share->getSharedWithDisplayName();
$extra["available"] = $availableExtra;
return $extra;
}
/**
* Get list extra permissions by shares
*/
public function getExtras(array $shares): array {
$result = [];
$shareIds = [];
foreach ($shares as $share) {
$shareIds[] = $share->getId();
}
if (empty($shareIds)) {
return $result;
}
$extras = $this->getList($shareIds);
$noActualList = [];
foreach ($shares as $share) {
$currentExtra = [];
foreach ($extras as $extra) {
if ($extra["share_id"] === $share->getId()) {
$currentExtra = $extra;
}
}
$wasInit = isset($currentExtra["permissions"]);
$checkExtra = $wasInit ? (int)$currentExtra["permissions"] : self::NONE;
[$availableExtra, $defaultPermissions] = $this->validation($share, $checkExtra, $wasInit);
if (($availableExtra === 0 || ($availableExtra & $checkExtra) !== $checkExtra) & & !empty($currentExtra)) {
$noActualList[] = $share->getId();
$currentExtra = [];
}
if ($availableExtra > 0) {
if (empty($currentExtra)) {
$currentExtra["id"] = -1;
$currentExtra["share_id"] = $share->getId();
$currentExtra["permissions"] = $defaultPermissions;
}
$currentExtra["type"] = $share->getShareType();
$currentExtra["shareWith"] = $share->getSharedWith();
$currentExtra["shareWithName"] = $share->getSharedWithDisplayName();
$currentExtra["available"] = $availableExtra;
if ($currentExtra["type"] === IShare::TYPE_ROOM & & $this->talkManager !== null) {
$rooms = $this->talkManager->searchRoomsByToken($currentExtra["shareWith"]);
if (!empty($rooms)) {
$room = $rooms[0];
$currentExtra["shareWith"] = $room->getName();
$currentExtra["shareWithName"] = $room->getName();
}
}
$result[] = $currentExtra;
}
}
if (!empty($noActualList)) {
$this->deleteList($noActualList);
}
return $result;
}
/**
* Get extra permissions by share
*
* @param string $shareId - share identifier
* @param integer $permissions - extra permissions bitmask
* @param integer $extraId - extra permission identifier
*
* @return bool
*/
public function setExtra(string $shareId, int $permissions, int $extraId): bool {
$result = false;
$share = $this->getShare($shareId);
if (!$share instanceof IShare) {
return $result;
}
[$availableExtra, $defaultPermissions] = $this->validation($share, $permissions);
if (($availableExtra & $permissions) !== $permissions) {
$this->logger->debug("Share " . $shareId . " does not available to extend permissions");
return $result;
}
$result = $extraId > 0 ? $this->update($share->getId(), $permissions) : $this->insert($share->getId(), $permissions);
return $result;
}
/**
* Delete extra permissions for share
*/
public function delete(string $shareId): bool {
$delete = $this->connection->prepare("
DELETE FROM `*PREFIX*" . self::TABLENAME_KEY . "`
WHERE `share_id` = ?
");
return (bool)$delete->execute([$shareId]);
}
/**
* Delete list extra permissions
*
* @param array $shareIds - array of share identifiers
*/
public function deleteList(array $shareIds): bool {
$condition = "";
if (count($shareIds) > 1) {
$counter = count($shareIds);
for ($i = 1; $i < $counter; $i++) {
$condition .= " OR `share_id` = ?";
}
}
$delete = $this->connection->prepare("
DELETE FROM `*PREFIX*" . self::TABLENAME_KEY . "`
WHERE `share_id` = ?
" . $condition);
return (bool)$delete->execute($shareIds);
}
/**
* Get extra permissions for share
*/
private function get(string $shareId): array {
$select = $this->connection->prepare("
SELECT id, share_id, permissions
FROM `*PREFIX*" . self::TABLENAME_KEY . "`
WHERE `share_id` = ?
");
$result = $select->execute([$shareId]);
$values = $result->fetch();
$value = is_array($values) ? $values : [];
$result = [];
if (!empty($value)) {
$result = [
"id" => (int)$value["id"],
"share_id" => (string)$value["share_id"],
"permissions" => (int)$value["permissions"]
];
}
return $result;
}
/**
* Get list extra permissions
*/
private function getList(array $shareIds): array {
$condition = "";
if (count($shareIds) > 1) {
$counter = count($shareIds);
for ($i = 1; $i < $counter; $i++) {
$condition .= " OR `share_id` = ?";
}
}
$select = $this->connection->prepare("
SELECT id, share_id, permissions
FROM `*PREFIX*" . self::TABLENAME_KEY . "`
WHERE `share_id` = ?
" . $condition);
$result = $select->execute($shareIds);
$values = $result->fetchAll();
$result = [];
if (is_array($values)) {
foreach ($values as $value) {
$result[] = [
"id" => (int)$value["id"],
"share_id" => (string)$value["share_id"],
"permissions" => (int)$value["permissions"]
];
}
}
return $result;
}
/**
* Store extra permissions for share
*
* @param string $shareId - share identifier
* @param integer $permissions - permissions bitmask
*/
private function insert(string $shareId, int $permissions): bool {
$insert = $this->connection->prepare("
INSERT INTO `*PREFIX*" . self::TABLENAME_KEY . "`
(`share_id`, `permissions`)
VALUES (?, ?)
");
return (bool)$insert->execute([$shareId, $permissions]);
}
/**
* Update extra permissions for share
*
* @param string $shareId - share identifier
* @param int $permissions - permissions bitmask
*/
private function update(string $shareId, int $permissions): bool {
$update = $this->connection->prepare("
UPDATE `*PREFIX*" . self::TABLENAME_KEY . "`
SET `permissions` = ?
WHERE `share_id` = ?
");
return (bool)$update->execute([$permissions, $shareId]);
}
/**
* Validation share on extend capability by extra permissions
*
* @param IShare $share - share
* @param int $checkExtra - checkable extra permissions bitmask
* @param bool $wasInit - was initialization extra
*/
private function validation(IShare $share, int $checkExtra, bool $wasInit = true): array {
$availableExtra = self::NONE;
$defaultExtra = self::NONE;
if ($share->getShareType() !== IShare::TYPE_LINK
& & ($share->getPermissions() & Constants::PERMISSION_SHARE) === Constants::PERMISSION_SHARE
& & $this->config->getValueString('core', 'shareapi_allow_resharing', 'yes') === 'yes') {
return [$availableExtra, $defaultExtra];
}
$node = $share->getNode();
$ext = strtolower(pathinfo((string) $node->getName(), PATHINFO_EXTENSION));
$format = !empty($ext) & & array_key_exists($ext, $this->appConfig->formatsSetting()) ? $this->appConfig->formatsSetting()[$ext] : null;
if (!isset($format)) {
return [$availableExtra, $defaultExtra];
}
if (($share->getPermissions() & Constants::PERMISSION_UPDATE) === Constants::PERMISSION_UPDATE) {
if (isset($format["modifyFilter"]) & & $format["modifyFilter"]
& & ($checkExtra & self::COMMENT) !== self::COMMENT) {
$availableExtra |= self::MODIFYFILTER;
$defaultExtra |= self::MODIFYFILTER;
}
if (isset($format["review"]) & & $format["review"]) {
$availableExtra |= self::REVIEW;
}
if (isset($format["comment"]) & & $format["comment"]
& & ($checkExtra & self::REVIEW) !== self::REVIEW
& & (($checkExtra & self::MODIFYFILTER) !== self::MODIFYFILTER)) {
$availableExtra |= self::COMMENT;
}
if (isset($format["fillForms"]) & & $format["fillForms"]
& & ($checkExtra & self::REVIEW) !== self::REVIEW) {
$availableExtra |= self::FILLFORMS;
}
if (!$wasInit & & ($defaultExtra & self::MODIFYFILTER) === self::MODIFYFILTER) {
$availableExtra ^= self::COMMENT;
}
}
return [$availableExtra, $defaultExtra];
}
/**
* Get origin share
*/
private function getShare(string $shareId): ?IShare {
foreach (["ocinternal", "ocRoomShare", "ocCircleShare"] as $provider) {
try {
return $this->shareManager->getShareById("$provider:$shareId");
} catch (ShareNotFound) {}
}
$this->logger->error("getShare: share not found: $shareId");
return null;
}
}