You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
294 lines
9.5 KiB
294 lines
9.5 KiB
<?php
|
|
/**
|
|
*
|
|
* (c) Copyright Ascensio System SIA 2025
|
|
*
|
|
* 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.
|
|
* In accordance with Section 7(a) of the GNU AGPL its Section 15 shall be amended to the effect
|
|
* that Ascensio System SIA expressly excludes the warranty of non-infringement of any third-party rights.
|
|
*
|
|
* 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: http://www.gnu.org/licenses/agpl-3.0.html
|
|
*
|
|
* You can contact Ascensio System SIA at 20A-12 Ernesta Birznieka-Upisha street, Riga, Latvia, EU, LV-1050.
|
|
*
|
|
* The interactive user interfaces in modified source and object code versions of the Program
|
|
* must display Appropriate Legal Notices, as required under Section 5 of the GNU AGPL version 3.
|
|
*
|
|
* Pursuant to Section 7(b) of the License you must retain the original Product logo when distributing the program.
|
|
* Pursuant to Section 7(e) we decline to grant you any rights under trademark law for use of our trademarks.
|
|
*
|
|
* All the Product's GUI elements, including illustrations and icon sets, as well as technical
|
|
* writing content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0 International.
|
|
* See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
|
|
*
|
|
*/
|
|
|
|
namespace OCA\Onlyoffice;
|
|
|
|
use OCA\Files_Sharing\External\Storage as SharingExternalStorage;
|
|
use OCP\Files\File;
|
|
use OCP\Http\Client\IClientService;
|
|
|
|
/**
|
|
* Remote instance manager
|
|
*
|
|
* @package OCA\Onlyoffice
|
|
*/
|
|
class RemoteInstance {
|
|
|
|
/**
|
|
* App name
|
|
*/
|
|
private const APP_NAME = "onlyoffice";
|
|
|
|
/**
|
|
* Table name
|
|
*/
|
|
private const TABLENAME_KEY = "onlyoffice_instance";
|
|
|
|
/**
|
|
* Time to live of remote instance (12 hours)
|
|
*/
|
|
private static $ttl = 60 * 60 * 1;
|
|
|
|
/**
|
|
* Health remote list
|
|
*/
|
|
private static $healthRemote = [];
|
|
|
|
/**
|
|
* Get remote instance
|
|
*
|
|
* @param string $remote - remote instance
|
|
*
|
|
* @return array
|
|
*/
|
|
private static function get($remote) {
|
|
$connection = \OC::$server->getDatabaseConnection();
|
|
$select = $connection->prepare("
|
|
SELECT remote, expire, status
|
|
FROM `*PREFIX*" . self::TABLENAME_KEY . "`
|
|
WHERE `remote` = ?
|
|
");
|
|
$result = $select->execute([$remote]);
|
|
|
|
$dbremote = $result ? $select->fetch() : [];
|
|
|
|
return $dbremote;
|
|
}
|
|
|
|
/**
|
|
* Store remote instance
|
|
*
|
|
* @param string $remote - remote instance
|
|
* @param bool $status - remote status
|
|
*
|
|
* @return bool
|
|
*/
|
|
private static function set($remote, $status) {
|
|
$connection = \OC::$server->getDatabaseConnection();
|
|
$insert = $connection->prepare("
|
|
INSERT INTO `*PREFIX*" . self::TABLENAME_KEY . "`
|
|
(`remote`, `status`, `expire`)
|
|
VALUES (?, ?, ?)
|
|
");
|
|
return (bool)$insert->execute([$remote, $status === true ? 1 : 0, time()]);
|
|
}
|
|
|
|
/**
|
|
* Update remote instance
|
|
*
|
|
* @param string $remote - remote instance
|
|
* @param bool $status - remote status
|
|
*
|
|
* @return bool
|
|
*/
|
|
private static function update($remote, $status) {
|
|
$connection = \OC::$server->getDatabaseConnection();
|
|
$update = $connection->prepare("
|
|
UPDATE `*PREFIX*" . self::TABLENAME_KEY . "`
|
|
SET status = ?, expire = ?
|
|
WHERE remote = ?
|
|
");
|
|
return (bool)$update->execute([$status === true ? 1 : 0, time(), $remote]);
|
|
}
|
|
|
|
/**
|
|
* Health check remote instance
|
|
*
|
|
* @param string $remote - remote instance
|
|
*
|
|
* @return bool
|
|
*/
|
|
public static function healthCheck($remote) {
|
|
$logger = \OCP\Log\logger('onlyoffice');
|
|
$remote = rtrim($remote, "/") . "/";
|
|
|
|
if (array_key_exists($remote, self::$healthRemote)) {
|
|
$logger->debug("Remote instance " . $remote . " from local cache", ["app" => self::APP_NAME]);
|
|
return self::$healthRemote[$remote];
|
|
}
|
|
|
|
$dbremote = self::get($remote);
|
|
if (!empty($dbremote) && $dbremote["expire"] + self::$ttl > time()) {
|
|
$logger->debug("Remote instance " . $remote . " from database status " . $dbremote["status"], ["app" => self::APP_NAME]);
|
|
self::$healthRemote[$remote] = $dbremote["status"];
|
|
return self::$healthRemote[$remote];
|
|
}
|
|
|
|
$httpClientService = \OC::$server->get(IClientService::class);
|
|
$client = $httpClientService->newClient();
|
|
|
|
$status = false;
|
|
try {
|
|
$response = $client->get($remote . "ocs/v2.php/apps/" . self::APP_NAME . "/api/v1/healthcheck?format=json");
|
|
$body = json_decode($response->getBody(), true);
|
|
|
|
$data = $body["ocs"]["data"];
|
|
|
|
if (isset($data["alive"])) {
|
|
$status = $data["alive"] === true;
|
|
}
|
|
} catch (\Exception $e) {
|
|
$logger->error("Failed to request federated health check for" . $remote, ['exception' => $e]);
|
|
}
|
|
|
|
if (empty($dbremote)) {
|
|
self::set($remote, $status);
|
|
} else {
|
|
self::update($remote, $status);
|
|
}
|
|
|
|
$logger->debug("Remote instance " . $remote . " was stored to database status " . $dbremote["status"], ["app" => self::APP_NAME]);
|
|
|
|
self::$healthRemote[$remote] = $status;
|
|
|
|
return self::$healthRemote[$remote];
|
|
}
|
|
|
|
/**
|
|
* Generate unique document identifier in federated share
|
|
*
|
|
* @param File $file - file
|
|
*
|
|
* @return string
|
|
*/
|
|
public static function getRemoteKey($file) {
|
|
$logger = \OCP\Log\logger('onlyoffice');
|
|
|
|
$remote = rtrim($file->getStorage()->getRemote(), "/") . "/";
|
|
$shareToken = $file->getStorage()->getToken();
|
|
$internalPath = $file->getInternalPath();
|
|
|
|
$httpClientService = \OC::$server->get(IClientService::class);
|
|
$client = $httpClientService->newClient();
|
|
|
|
try {
|
|
$response = $client->post($remote . "ocs/v2.php/apps/" . self::APP_NAME . "/api/v1/key?format=json", [
|
|
"timeout" => 5,
|
|
"body" => [
|
|
"shareToken" => $shareToken,
|
|
"path" => $internalPath
|
|
]
|
|
]);
|
|
|
|
$body = \json_decode($response->getBody(), true);
|
|
|
|
$data = $body["ocs"]["data"];
|
|
if (!empty($data["error"])) {
|
|
$logger->error("Error federated key " . $data["error"], ["app" => self::APP_NAME]);
|
|
return null;
|
|
}
|
|
|
|
$key = $data["key"];
|
|
$logger->debug("Federated key: $key", ["app" => self::APP_NAME]);
|
|
|
|
return $key;
|
|
} catch (\Exception $e) {
|
|
$logger->error("Failed to request federated key " . $file->getId(), ['exception' => $e]);
|
|
|
|
if ($e->getResponse()->getStatusCode() === 404) {
|
|
self::update($remote, false);
|
|
$logger->debug("Changed status for remote instance $remote to false", ["app" => self::APP_NAME]);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Change lock status in the federated share
|
|
*
|
|
* @param File $file - file
|
|
* @param bool $lock - status
|
|
* @param bool $fs - status
|
|
*
|
|
* @return bool
|
|
*/
|
|
public static function lockRemoteKey($file, $lock, $fs) {
|
|
$logger = \OCP\Log\logger('onlyoffice');
|
|
$action = $lock ? "lock" : "unlock";
|
|
|
|
$remote = rtrim($file->getStorage()->getRemote(), "/") . "/";
|
|
$shareToken = $file->getStorage()->getToken();
|
|
$internalPath = $file->getInternalPath();
|
|
|
|
$httpClientService = \OC::$server->get(IClientService::class);
|
|
$client = $httpClientService->newClient();
|
|
$data = [
|
|
"timeout" => 5,
|
|
"body" => [
|
|
"shareToken" => $shareToken,
|
|
"path" => $internalPath,
|
|
"lock" => $lock
|
|
]
|
|
];
|
|
if (!empty($fs)) {
|
|
$data["body"]["fs"] = $fs;
|
|
}
|
|
|
|
try {
|
|
$response = $client->post($remote . "ocs/v2.php/apps/" . self::APP_NAME . "/api/v1/keylock?format=json", $data);
|
|
$body = \json_decode($response->getBody(), true);
|
|
|
|
$data = $body["ocs"]["data"];
|
|
|
|
if (empty($data)) {
|
|
$logger->debug("Federated request " . $action . " for " . $file->getFileInfo()->getId() . " is successful", ["app" => self::APP_NAME]);
|
|
return true;
|
|
}
|
|
|
|
if (!empty($data["error"])) {
|
|
$logger->error("Error " . $action . " federated key for " . $file->getFileInfo()->getId() . ": " . $data["error"], ["app" => self::APP_NAME]);
|
|
return false;
|
|
}
|
|
} catch (\Exception $e) {
|
|
$logger->error("Failed to request federated " . $action . " for " . $file->getFileInfo()->getId(), ['exception' => $e]);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check of federated capable
|
|
*
|
|
* @param File $file - file
|
|
*
|
|
* @return bool
|
|
*/
|
|
public static function isRemoteFile($file) {
|
|
$storage = $file->getStorage();
|
|
|
|
$alive = false;
|
|
$isFederated = $storage->instanceOfStorage(SharingExternalStorage::class);
|
|
if (!$isFederated) {
|
|
return false;
|
|
}
|
|
|
|
$alive = RemoteInstance::healthCheck($storage->getRemote());
|
|
return $alive;
|
|
}
|
|
}
|
|
|