diff --git a/appinfo/app.php b/appinfo/app.php index 788b4c78..b45c7211 100644 --- a/appinfo/app.php +++ b/appinfo/app.php @@ -21,19 +21,19 @@ return [ // The string under which your app will be referenced in owncloud - 'id' => 'sciencemesh', + "id" => "sciencemesh", // The sorting weight for the navigation. // The higher the number, the higher will it be listed in the navigation - 'order' => 10, + "order" => 10, // The route that will be shown on startup - 'href' => $urlGenerator->linkToRoute('sciencemesh.app.contacts'), + "href" => $urlGenerator->linkToRoute("sciencemesh.app.contacts"), // The icon that will be shown in the navigation, located in img/ - 'icon' => $urlGenerator->imagePath('sciencemesh', 'app-white.svg'), + "icon" => $urlGenerator->imagePath("sciencemesh", "app-white.svg"), // The application's title, used in the navigation & the settings page of your app - 'name' => OC::$server->getL10N('sciencemesh')->t('ScienceMesh'), + "name" => OC::$server->getL10N("sciencemesh")->t("ScienceMesh"), ]; }); diff --git a/lib/Controller/AuthController.php b/lib/Controller/AuthController.php index 725301c2..caef9518 100644 --- a/lib/Controller/AuthController.php +++ b/lib/Controller/AuthController.php @@ -16,6 +16,7 @@ use OC\Config; use OCA\ScienceMesh\ServerConfig; use OCA\ScienceMesh\ShareProvider\ScienceMeshShareProvider; +use OCA\ScienceMesh\Utils\Utils; use OCP\AppFramework\Controller; use OCP\AppFramework\Http; use OCP\AppFramework\Http\JSONResponse; @@ -44,6 +45,9 @@ class AuthController extends Controller /** @var IUserManager */ private IUserManager $userManager; + /** @var Utils */ + private Utils $utils; + /** @var ScienceMeshShareProvider */ private ScienceMeshShareProvider $shareProvider; @@ -79,22 +83,7 @@ public function __construct( $this->logger = $logger; $this->userManager = $userManager; $this->shareProvider = $shareProvider; - } - - // TODO: @Mahdi Move to utils. - - /** - * @throws NotPermittedException - * @throws Exception - */ - private function checkRevadAuth() - { - error_log("checkRevadAuth"); - $authHeader = $this->request->getHeader('X-Reva-Secret'); - - if ($authHeader != $this->config->getRevaSharedSecret()) { - throw new NotPermittedException('Please set an http request header "X-Reva-Secret: "!'); - } + $this->utils = new Utils($l10n, $logger, $shareProvider); } /** @@ -111,7 +100,7 @@ public function Authenticate($userId): JSONResponse { error_log("Authenticate: " . $userId); - $this->checkRevadAuth(); + $this->utils->checkRevadAuth($this->request, $this->config->getRevaSharedSecret()); if ($this->userManager->userExists($userId)) { $userId = $this->request->getParam("clientID"); @@ -143,7 +132,7 @@ public function Authenticate($userId): JSONResponse // "path": "some/file/path.txt" // } $result = [ - "user" => $this->formatUser($user), + "user" => $this->utils->formatUser($user, $this->config->getIopIdp()), "scopes" => [ "user" => [ "resource" => [ @@ -160,19 +149,4 @@ public function Authenticate($userId): JSONResponse return new JSONResponse("Username / password not recognized", Http::STATUS_UNAUTHORIZED); } - - // TODO: @Mahdi Move to utils. - private function formatUser($user): array - { - return [ - "id" => [ - "idp" => $this->config->getIopIdp(), - "opaque_id" => $user->getUID(), - ], - "display_name" => $user->getDisplayName(), - "username" => $user->getUID(), - "email" => $user->getEmailAddress(), - "type" => 1, - ]; - } } diff --git a/lib/Controller/OcmController.php b/lib/Controller/OcmController.php index a82052c4..8697806f 100644 --- a/lib/Controller/OcmController.php +++ b/lib/Controller/OcmController.php @@ -14,9 +14,11 @@ use DateTime; use Exception; use OC\Config; +use OC\HintException; use OCA\ScienceMesh\AppInfo\ScienceMeshApp; use OCA\ScienceMesh\ServerConfig; use OCA\ScienceMesh\ShareProvider\ScienceMeshShareProvider; +use OCA\ScienceMesh\Utils\Utils; use OCP\AppFramework\Controller; use OCP\AppFramework\Http; use OCP\AppFramework\Http\DataResponse; @@ -25,7 +27,6 @@ use OCP\Files\Folder; use OCP\Files\InvalidPathException; use OCP\Files\IRootFolder; -use OCP\Files\Node; use OCP\Files\NotFoundException; use OCP\Files\NotPermittedException; use OCP\IConfig; @@ -33,18 +34,9 @@ use OCP\ILogger; use OCP\IRequest; use OCP\IUserManager; -use OCP\Lock\ILockingProvider; -use OCP\Lock\LockedException; use OCP\Share\Exceptions\IllegalIDChangeException; use OCP\Share\Exceptions\ShareNotFound; use OCP\Share\IManager; -use OCP\Share\IShare; - -define("RESTRICT_TO_SCIENCEMESH_FOLDER", false); -define("EFSS_PREFIX", (RESTRICT_TO_SCIENCEMESH_FOLDER ? "sciencemesh/" : "")); - -// See https://github.com/pondersource/sciencemesh-php/issues/96#issuecomment-1298656896 -define("REVA_PREFIX", "/home/"); class OcmController extends Controller { @@ -60,9 +52,6 @@ class OcmController extends Controller /** @var IManager */ private IManager $shareManager; - /** @var ScienceMeshShareProvider */ - private ScienceMeshShareProvider $shareProvider; - /** @var Folder */ private Folder $userFolder; @@ -72,6 +61,12 @@ class OcmController extends Controller /** @var ILogger */ private ILogger $logger; + /** @var Utils */ + private Utils $utils; + + /** @var ScienceMeshShareProvider */ + private ScienceMeshShareProvider $shareProvider; + /** * Open Cloud Mesh (OCM) Controller. * @@ -108,6 +103,7 @@ public function __construct( $this->l = $l10n; $this->logger = $logger; $this->shareProvider = $shareProvider; + $this->utils = new Utils($l10n, $logger, $shareProvider); } /** @@ -117,7 +113,7 @@ public function __construct( private function init($userId) { error_log("RevaController init for user '$userId'"); - $this->checkRevadAuth(); + $this->utils->checkRevadAuth($this->request, $this->config->getRevaSharedSecret()); if ($userId) { error_log("root folder absolute path '" . $this->rootFolder->getPath() . "'"); if ($this->rootFolder->nodeExists($userId)) { @@ -129,230 +125,6 @@ private function init($userId) } } - // TODO: @Mahdi Move to utils. - - /** - * @throws NotPermittedException - * @throws Exception - */ - private function checkRevadAuth() - { - error_log("checkRevadAuth"); - $authHeader = $this->request->getHeader('X-Reva-Secret'); - - if ($authHeader != $this->config->getRevaSharedSecret()) { - throw new NotPermittedException('Please set an http request header "X-Reva-Secret: "!'); - } - } - - // TODO: @Mahdi Move to utils. - private function revaPathToEfssPath($revaPath): string - { - if ("$revaPath/" == REVA_PREFIX) { - error_log("revaPathToEfssPath: Interpreting special case $revaPath as ''"); - return ''; - } - $ret = EFSS_PREFIX . $this->removePrefix($revaPath, REVA_PREFIX); - error_log("revaPathToEfssPath: Interpreting $revaPath as $ret"); - return $ret; - } - - // TODO: @Mahdi Move to utils. - private function revaPathFromOpaqueId($opaqueId) - { - return $this->removePrefix($opaqueId, "fileid-"); - } - - // TODO: @Mahdi Move to utils. - private function removePrefix($string, $prefix) - { - // first check if string is actually prefixed or not. - $len = strlen($prefix); - if (substr($string, 0, $len) === $prefix) { - $ret = substr($string, $len); - } else { - $ret = $string; - } - - return $ret; - } - - // TODO: @Mahdi Move to utils. - private function getPermissionsCode(array $permissions): int - { - $permissionsCode = 0; - if (!empty($permissions["get_path"]) || !empty($permissions["get_quota"]) || !empty($permissions["initiate_file_download"]) || !empty($permissions["initiate_file_upload"]) || !empty($permissions["stat"])) { - $permissionsCode += Constants::PERMISSION_READ; - } - if (!empty($permissions["create_container"]) || !empty($permissions["move"]) || !empty($permissions["add_grant"]) || !empty($permissions["restore_file_version"]) || !empty($permissions["restore_recycle_item"])) { - $permissionsCode += Constants::PERMISSION_CREATE; - } - if (!empty($permissions["move"]) || !empty($permissions["delete"]) || !empty($permissions["remove_grant"])) { - $permissionsCode += Constants::PERMISSION_DELETE; - } - if (!empty($permissions["list_grants"]) || !empty($permissions["list_file_versions"]) || !empty($permissions["list_recycle"])) { - $permissionsCode += Constants::PERMISSION_SHARE; - } - if (!empty($permissions["update_grant"])) { - $permissionsCode += Constants::PERMISSION_UPDATE; - } - return $permissionsCode; - } - - // TODO: @Mahdi Move to utils. - - /** - * @param Node $node - * @return void - * - * @throws LockedException - */ - private function lock(Node $node) - { - $node->lock(ILockingProvider::LOCK_SHARED); - } - - // TODO: @Mahdi Move to utils. - // For ListReceivedShares, GetReceivedShare and UpdateReceivedShare we need to include "state:2" - // see: - // https://github.com/cs3org/cs3apis/blob/cfd1ad29fdf00c79c2a321de7b1a60d0725fe4e8/cs3/sharing/ocm/v1beta1/resources.proto#L160 - /** - * @throws NotFoundException - * @throws InvalidPathException - */ - private function shareInfoToCs3Share(IShare $share, string $direction, $token = ""): array - { - $shareId = $share->getId(); - - // TODO @Mahdi use enums! - if ($direction === "sent") { - $ocmShareData = $this->shareProvider->getSentOcmShareFromSciencemeshTable($shareId); - $ocmShareProtocols = $this->shareProvider->getSentOcmShareProtocolsFromSciencemeshTable($ocmShareData["id"]); - } elseif ($direction === "received") { - $ocmShareData = $this->shareProvider->getReceivedOcmShareFromSciencemeshTable($shareId); - $ocmShareProtocols = $this->shareProvider->getReceivedOcmShareProtocolsFromSciencemeshTable($ocmShareData["id"]); - } - - // use ocm payload stored in sciencemesh table. if it fails, use native efss share data. - // in case of total failure use "unknown". - - // this one is obvious right? - if (isset($ocmShareData["share_with"])) { - $granteeParts = explode("@", $ocmShareData["share_with"]); - } else { - $granteeParts = explode("@", $share->getSharedWith()); - } - - if (count($granteeParts) != 2) { - $granteeParts = ["unknown", "unknown"]; - } - - // the original share owner (who owns the path that is shared) - if (isset($ocmShareData["owner"])) { - $ownerParts = explode("@", $ocmShareData["owner"]); - } else { - $ownerParts = explode("@", $share->getShareOwner()); - } - - if (count($granteeParts) != 2) { - $ownerParts = ["unknown", "unknown"]; - } - - // NOTE: @Mahdi initiator/creator/sharedBy etc., whatever other names it has! means the share sharer! - // you can be owner and sharer, you can be someone who is re-sharing, in this case you are sharer but not owner - if (isset($ocmShareData["initiator"])) { - $creatorParts = explode("@", $ocmShareData["initiator"]); - } else { - $creatorParts = explode("@", $share->getSharedBy()); - } - - if (count($granteeParts) != 2) { - $creatorParts = ["unknown", "unknown"]; - } - - try { - $filePath = $share->getNode()->getPath(); - // @Mahdi why is this hardcoded? - // @Giuseppe this should be something that doesn't change when file is moved! - $opaqueId = "fileid-" . $filePath; - } catch (NotFoundException $e) { - // @Mahdi why not just return status bad request or status not found? - // @Michiel sometimes you want to translate share object even if file doesn't exist. - $opaqueId = "unknown"; - } - - // TODO: @Mahdi update this comment to point at the Reva structure mappings for this json. - // produces JSON that maps to reva - $payload = [ - // use OCM name, if null use efss share native name, if null fall back to "unknown" - "name" => $ocmShareData["name"] ?? ($share->getName() ?? "unknown"), - "token" => $token ?? "unknown", - // TODO: @Mahdi what permissions is the correct one? share permissions has different value than the share->node permissions. - // maybe use the ocmData for this one? needs testing for different scenarios to see which is the best/correct one. - "permissions" => $share->getNode()->getPermissions() ?? 0, - "id" => [ - // https://github.com/cs3org/go-cs3apis/blob/d297419/cs3/sharing/ocm/v1beta1/resources.pb.go#L423 - "opaque_id" => $shareId ?? "unknown", - ], - "resource_id" => [ - "opaque_id" => $opaqueId, - ], - // these three have been already handled and don't need "unknown" default values. - "grantee" => [ - "id" => [ - "opaque_id" => $granteeParts[0], - "idp" => $granteeParts[1], - ], - ], - "owner" => [ - "id" => [ - "opaque_id" => $ownerParts[0], - "idp" => $ownerParts[1], - ], - ], - "creator" => [ - "id" => [ - "opaque_id" => $creatorParts[0], - "idp" => $creatorParts[1], - ], - ], - // NOTE: make sure seconds type is int, otherwise Reva gives: - // error="json: cannot unmarshal string into Go struct field Timestamp.ctime.seconds of type uint64" - "ctime" => [ - "seconds" => isset($ocmShareData["ctime"]) ? (int)$ocmShareData["ctime"] : ($share->getShareTime()->getTimestamp() ?? 0) - ], - "mtime" => [ - "seconds" => isset($ocmShareData["mtime"]) ? (int)$ocmShareData["ctime"] : ($share->getShareTime()->getTimestamp() ?? 0) - ], - "access_methods" => [ - "transfer" => [ - "source_uri" => $ocmShareProtocols["transfer"]["source_uri"] ?? "unknown", - // TODO: @Mahdi this feels redundant, already included in top-level token and webdav shared_secret. - "shared_secret" => $ocmShareProtocols["transfer"]["shared_secret"] ?? "unknown", - // TODO: @Mahdi should the default value be an integer? - "size" => $ocmShareProtocols["transfer"]["size"] ?? "unknown", - ], - "webapp" => [ - "uri_template" => $ocmShareProtocols["webapp"]["uri_template"] ?? "unknown", - "view_mode" => $ocmShareProtocols["webapp"]["view_mode"] ?? "unknown", - ], - "webdav" => [ - // TODO: @Mahdi it is better to have sharedSecret and permissions in this part of code. - "uri" => $ocmShareProtocols["webdav"]["uri"] ?? "unknown", - // TODO: @Mahdi it is interesting this function accepts token as argument! is token different that the share secret? - // why do we have to pass token while the share object already has the information about token? - // $share->getToken(); - "shared_secret" => $ocmShareProtocols["webdav"]["shared_secret"] ?? "unknown", - "permissions" => $ocmShareProtocols["webdav"]["permissions"] ?? "unknown", - ], - ] - ]; - - error_log("shareInfoToCs3Share " . var_export($payload, true)); - - return $payload; - } - /** * add a received share. * @@ -460,7 +232,7 @@ public function addReceivedShare($userId): JSONResponse } // convert permissions from array to integer. - $integerPermissions = $this->getPermissionsCode($ocmProtocolWebdav["permissions"]["permissions"]); + $integerPermissions = $this->utils->getPermissionsCode($ocmProtocolWebdav["permissions"]["permissions"]); $ocmProtocolWebdav["permissions"] = $integerPermissions; $sharedSecret = $ocmProtocolWebdav["sharedSecret"]; @@ -577,8 +349,8 @@ public function addSentShare($userId) // chained path conversions to verify this file exists in our server. // "fileid-/home/test" -> "/home/test" -> "/test" - $revaPath = $this->revaPathFromOpaqueId($resourceId); - $efssPath = $this->revaPathToEfssPath($revaPath); + $revaPath = $this->utils->revaPathFromOpaqueId($resourceId); + $efssPath = $this->utils->revaPathToEfssPath($revaPath); try { $node = $this->userFolder->get($efssPath); @@ -677,7 +449,7 @@ public function addSentShare($userId) } // convert permissions from array to integer. - $permissions = $this->getPermissionsCode($ocmProtocolWebdav["permissions"]); + $permissions = $this->utils->getPermissionsCode($ocmProtocolWebdav["permissions"]); // prepare data for adding to the native efss table. $share = $this->shareManager->newShare(); @@ -723,7 +495,7 @@ public function addSentShare($userId) return new JSONResponse($message_t, Http::STATUS_UNPROCESSABLE_ENTITY); } - $this->lock($share->getNode()); + $this->utils->lock($share->getNode()); // prepare share data for ocm $share = $this->shareProvider->createNativeEfssScienceMeshShare($share); @@ -779,7 +551,7 @@ public function getReceivedShare($userId): JSONResponse try { $share = $this->shareProvider->getReceivedShareByToken($opaqueId); - $response = $this->shareInfoToCs3Share($share, "received", $opaqueId); + $response = $this->utils->shareInfoToCs3Share($share, "received", $opaqueId); $response["state"] = 2; return new JSONResponse($response, Http::STATUS_OK); } catch (Exception $e) { @@ -813,7 +585,7 @@ public function getSentShare($userId): JSONResponse $share = $this->shareProvider->getSentShareByName($userId, $name); if ($share) { - $response = $this->shareInfoToCs3Share($share, "sent"); + $response = $this->utils->shareInfoToCs3Share($share, "sent"); return new JSONResponse($response, Http::STATUS_OK); } @@ -828,11 +600,7 @@ public function getSentShare($userId): JSONResponse * @param $userId * @return JSONResponse * - * @throws InvalidPathException - * @throws NotFoundException * @throws NotPermittedException - * @throws ShareNotFound - * @throws IllegalIDChangeException */ public function getSentShareByToken($userId): JSONResponse { @@ -847,18 +615,30 @@ public function getSentShareByToken($userId): JSONResponse } } + // TODO: @Mahdi check for being null? $token = $this->request->getParam("Spec")["Token"]; error_log("GetSentShareByToken: " . var_export($this->request->getParam("Spec"), true)); - // TODO: @Mahdi handle in try catch block and send back correct responses. - $share = $this->shareProvider->getSentShareByToken($token); + try { + $share = $this->shareProvider->getSentShareByToken($token); + } catch (ShareNotFound|IllegalIDChangeException $e) { + // TODO: @Mahdi log it. + return new JSONResponse( + ["error" => "GetSentShareByToken failed! because: $e"], + Http::STATUS_BAD_REQUEST + ); + } - if ($share) { - $response = $this->shareInfoToCs3Share($share, "sent", $token); + try { + $response = $this->utils->shareInfoToCs3Share($share, "sent", $token); return new JSONResponse($response, Http::STATUS_OK); + } catch (NotFoundException|InvalidPathException $e) { + // TODO: @Mahdi log it. + return new JSONResponse( + ["error" => "GetSentShareByToken failed! because: $e"], + Http::STATUS_BAD_REQUEST + ); } - - return new JSONResponse(["error" => "GetSentShare failed"], Http::STATUS_BAD_REQUEST); } /** @@ -886,7 +666,7 @@ public function listReceivedShares($userId): JSONResponse if ($shares) { foreach ($shares as $share) { - $response = $this->shareInfoToCs3Share($share, "received"); + $response = $this->utils->shareInfoToCs3Share($share, "received"); $responses[] = [ "share" => $response, "state" => 2 @@ -924,7 +704,7 @@ public function listSentShares($userId): JSONResponse if ($shares) { foreach ($shares as $share) { - $responses[] = $this->shareInfoToCs3Share($share, "sent"); + $responses[] = $this->utils->shareInfoToCs3Share($share, "sent"); } } return new JSONResponse($responses, Http::STATUS_OK); @@ -987,13 +767,13 @@ public function updateReceivedShare($userId): JSONResponse $resourceId = $this->request->getParam("received_share")["share"]["resource_id"]; $permissions = $this->request->getParam("received_share")["share"]["permissions"]; - $permissionsCode = $this->getPermissionsCode($permissions); + $permissionsCode = $this->utils->getPermissionsCode($permissions); try { $share = $this->shareProvider->getReceivedShareByToken($resourceId); $share->setPermissions($permissionsCode); $shareUpdate = $this->shareProvider->UpdateReceivedShare($share); - $response = $this->shareInfoToCs3Share($shareUpdate, "received", $resourceId); + $response = $this->utils->shareInfoToCs3Share($shareUpdate, "received", $resourceId); $response["state"] = 2; return new JSONResponse($response, Http::STATUS_OK); } catch (Exception $e) { @@ -1011,6 +791,7 @@ public function updateReceivedShare($userId): JSONResponse * @throws InvalidPathException * @throws NotFoundException * @throws NotPermittedException + * @throws ShareNotFound|HintException */ public function updateSentShare($userId): JSONResponse { @@ -1023,7 +804,7 @@ public function updateSentShare($userId): JSONResponse $opaqueId = $this->request->getParam("ref")["Spec"]["Id"]["opaque_id"]; $permissions = $this->request->getParam("p")["permissions"]; - $permissionsCode = $this->getPermissionsCode($permissions); + $permissionsCode = $this->utils->getPermissionsCode($permissions); $name = $this->getNameByOpaqueId($opaqueId); if (!($share = $this->shareProvider->getSentShareByName($userId, $name))) { return new JSONResponse(["error" => "UpdateSentShare failed"], Http::STATUS_INTERNAL_SERVER_ERROR); @@ -1031,7 +812,7 @@ public function updateSentShare($userId): JSONResponse $share->setPermissions($permissionsCode); $shareUpdated = $this->shareProvider->update($share); - $response = $this->shareInfoToCs3Share($shareUpdated, "sent"); + $response = $this->utils->shareInfoToCs3Share($shareUpdated, "sent"); return new JSONResponse($response, Http::STATUS_OK); } } diff --git a/lib/Controller/StorageController.php b/lib/Controller/StorageController.php index f7f19da2..b6f4a889 100644 --- a/lib/Controller/StorageController.php +++ b/lib/Controller/StorageController.php @@ -16,12 +16,13 @@ use OC\Files\View; use OCA\DAV\TrashBin\TrashBinManager; use OCA\ScienceMesh\ServerConfig; +use OCA\ScienceMesh\ShareProvider\ScienceMeshShareProvider; +use OCA\ScienceMesh\Utils\Utils; use OCP\AppFramework\Controller; use OCP\AppFramework\Http; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\Http\JSONResponse; use OCP\AppFramework\Http\StreamResponse; -use OCP\Files\FileInfo; use OCP\Files\Folder; use OCP\Files\InvalidPathException; use OCP\Files\IRootFolder; @@ -36,10 +37,6 @@ use Symfony\Component\Filesystem\Exception\FileNotFoundException; define("RESTRICT_TO_SCIENCEMESH_FOLDER", false); -define("EFSS_PREFIX", (RESTRICT_TO_SCIENCEMESH_FOLDER ? "sciencemesh/" : "")); - -// See https://github.com/pondersource/sciencemesh-php/issues/96#issuecomment-1298656896 -define("REVA_PREFIX", "/home/"); class StorageController extends Controller { @@ -67,6 +64,9 @@ class StorageController extends Controller /** @var ILogger */ private ILogger $logger; + /** @var Utils */ + private Utils $utils; + /** * Storage Controller. * @@ -78,16 +78,18 @@ class StorageController extends Controller * @param TrashBinManager $trashManager * @param IL10N $l10n * @param ILogger $logger + * @param ScienceMeshShareProvider $shareProvider */ public function __construct( - string $appName, - IRootFolder $rootFolder, - IRequest $request, - IUserManager $userManager, - IConfig $config, - TrashBinManager $trashManager, - IL10N $l10n, - ILogger $logger + string $appName, + IRootFolder $rootFolder, + IRequest $request, + IUserManager $userManager, + IConfig $config, + TrashBinManager $trashManager, + IL10N $l10n, + ILogger $logger, + ScienceMeshShareProvider $shareProvider ) { parent::__construct($appName, $request); @@ -100,6 +102,7 @@ public function __construct( $this->trashManager = $trashManager; $this->l = $l10n; $this->logger = $logger; + $this->utils = new Utils($l10n, $logger, $shareProvider); } /** @@ -110,7 +113,7 @@ private function init($userId) { error_log("RevaController init for user '$userId'"); $this->userId = $userId; - $this->checkRevadAuth(); + $this->utils->checkRevadAuth($this->request, $this->config->getRevaSharedSecret()); if ($userId) { error_log("root folder absolute path '" . $this->rootFolder->getPath() . "'"); if ($this->rootFolder->nodeExists($userId)) { @@ -122,136 +125,6 @@ private function init($userId) } } - /** - * @throws NotPermittedException - * @throws Exception - */ - private function checkRevadAuth() - { - error_log("checkRevadAuth"); - $authHeader = $this->request->getHeader('X-Reva-Secret'); - - if ($authHeader != $this->config->getRevaSharedSecret()) { - throw new NotPermittedException('Please set an http request header "X-Reva-Secret: "!'); - } - } - - private function efssFullPathToRelativePath($efssFullPath) - { - - $ret = $this->removePrefix($efssFullPath, $this->userFolder->getPath()); - error_log("efssFullPathToRelativePath: Interpreting $efssFullPath as $ret"); - return $ret; - } - - private function efssPathToRevaPath($efssPath): string - { - $ret = REVA_PREFIX . $this->removePrefix($efssPath, EFSS_PREFIX); - error_log("efssPathToRevaPath: Interpreting $efssPath as $ret"); - return $ret; - } - - private function revaPathToEfssPath($revaPath): string - { - if ("$revaPath/" == REVA_PREFIX) { - error_log("revaPathToEfssPath: Interpreting special case $revaPath as ''"); - return ''; - } - $ret = EFSS_PREFIX . $this->removePrefix($revaPath, REVA_PREFIX); - error_log("revaPathToEfssPath: Interpreting $revaPath as $ret"); - return $ret; - } - - private function revaPathFromOpaqueId($opaqueId) - { - return $this->removePrefix($opaqueId, "fileid-"); - } - - private function removePrefix($string, $prefix) - { - // first check if string is actually prefixed or not. - $len = strlen($prefix); - if (substr($string, 0, $len) === $prefix) { - $ret = substr($string, $len); - } else { - $ret = $string; - } - - return $ret; - } - - private function getChecksum(Node $node, $checksumType = 4): string - { - $checksumTypes = array( - 1 => "UNSET:", - 2 => "ADLER32:", - 3 => "MD5:", - 4 => "SHA1:", - ); - - // checksum is in db table oc_filecache. - // folders do not have checksum - $checksums = explode(" ", $node->getFileInfo()->getChecksum()); - - foreach ($checksums as $checksum) { - - // NOTE: that the use of !== false is deliberate (neither != false nor === true will return the desired result); - // strpos() returns either the offset at which the needle string begins in the haystack string, or the boolean - // false if the needle isn't found. Since 0 is a valid offset and 0 is "false", we can't use simpler constructs - // like !strpos($a, 'are'). - if (strpos($checksum, $checksumTypes[$checksumType]) !== false) { - return substr($checksum, strlen($checksumTypes[$checksumType])); - } - } - - return ""; - } - - /** - * @throws InvalidPathException - * @throws NotFoundException - */ - private function nodeToCS3ResourceInfo(Node $node): array - { - $isDirectory = ($node->getType() === FileInfo::TYPE_FOLDER); - $efssPath = substr($node->getPath(), strlen($this->userFolder->getPath()) + 1); - $revaPath = $this->efssPathToRevaPath($efssPath); - - $payload = [ - "type" => ($isDirectory ? 2 : 1), - "id" => [ - "opaque_id" => "fileid-" . $revaPath, - ], - "checksum" => [ - // checksum algorithms: - // 1 UNSET - // 2 ADLER32 - // 3 MD5 - // 4 SHA1 - - // NOTE: folders do not have checksum, their type should be unset. - "type" => $isDirectory ? 1 : 4, - "sum" => $this->getChecksum($node, $isDirectory ? 1 : 4), - ], - "etag" => $node->getEtag(), - "mime_type" => ($isDirectory ? "folder" : $node->getMimetype()), - "mtime" => [ - "seconds" => $node->getMTime(), - ], - "path" => $revaPath, - "permissions" => $node->getPermissions(), - "size" => $node->getSize(), - "owner" => [ - "opaque_id" => $this->userId, - "idp" => $this->config->getIopIdp(), - ] - ]; - - error_log("nodeToCS3ResourceInfo " . var_export($payload, true)); - - return $payload; - } - /** * @PublicPage * @NoAdminRequired @@ -269,7 +142,7 @@ public function addGrant($userId): JSONResponse return new JSONResponse("User not found", Http::STATUS_FORBIDDEN); } - $path = $this->revaPathToEfssPath($this->request->getParam("path")); + $path = $this->utils->revaPathToEfssPath($this->request->getParam("path")); // FIXME: Expected a param with a grant to add here; return new JSONResponse("Not implemented", Http::STATUS_NOT_IMPLEMENTED); @@ -293,7 +166,7 @@ public function createDir($userId): JSONResponse } $urlDecodedPath = urldecode($this->request->getParam("path")); - $path = $this->revaPathToEfssPath($urlDecodedPath); + $path = $this->utils->revaPathToEfssPath($urlDecodedPath); try { $this->userFolder->newFolder($path); @@ -325,7 +198,10 @@ public function createHome($userId): JSONResponse try { $this->userFolder->newFolder("sciencemesh"); // Create the Sciencemesh directory for storage if it doesn't exist. } catch (NotPermittedException $e) { - return new JSONResponse(["error" => "Create home failed. Resource Path not founD"], Http::STATUS_INTERNAL_SERVER_ERROR); + return new JSONResponse( + ["error" => "Create home failed. Resource Path not found"], + Http::STATUS_INTERNAL_SERVER_ERROR + ); } return new JSONResponse("CREATED", Http::STATUS_CREATED); } @@ -349,7 +225,7 @@ public function createReference($userId): JSONResponse } else { return new JSONResponse("User not found", Http::STATUS_FORBIDDEN); } - $path = $this->revaPathToEfssPath($this->request->getParam("path")); + $path = $this->utils->revaPathToEfssPath($this->request->getParam("path")); return new JSONResponse("Not implemented", Http::STATUS_NOT_IMPLEMENTED); } @@ -426,7 +302,7 @@ public function delete($userId): JSONResponse return new JSONResponse("User not found", Http::STATUS_FORBIDDEN); } - $path = $this->revaPathToEfssPath($this->request->getParam("path")); + $path = $this->utils->revaPathToEfssPath($this->request->getParam("path")); try { $node = $this->userFolder->get($path); @@ -455,7 +331,7 @@ public function download($userId, $path) error_log("Download path: $path"); - $efssPath = $this->removePrefix($path, "home/"); + $efssPath = $this->utils->removePrefix($path, "home/"); error_log("Download efss path: $efssPath"); if ($this->userFolder->nodeExists($efssPath)) { @@ -543,7 +419,7 @@ public function getMD($userId): JSONResponse // }, // "mdKeys": null // } - $revaPath = $this->revaPathFromOpaqueId($ref["resource_id"]["opaque_id"]); + $revaPath = $this->utils->revaPathFromOpaqueId($ref["resource_id"]["opaque_id"]); } else { return new JSONResponse("ref not understood!", Http::STATUS_BAD_REQUEST); } @@ -552,19 +428,24 @@ public function getMD($userId): JSONResponse // for example this converts "we%20have%20space" to "we have space" $revaPathDecoded = urldecode($revaPath); - $path = $this->revaPathToEfssPath($revaPathDecoded); + $path = $this->utils->revaPathToEfssPath($revaPathDecoded); error_log("Looking for EFSS path '$path' in user folder; reva path '$revaPathDecoded' "); // apparently nodeExists requires relative path to the user folder: // see https://github.com/owncloud/core/blob/b7bcbdd9edabf7d639b4bb42c4fb87862ddf4a80/lib/private/Files/Node/Folder.php#L45-L55; // another string manipulation is necessary to extract relative path from full path. - $relativePath = $this->efssFullPathToRelativePath($path); + $relativePath = $this->utils->efssFullPathToRelativePath($path, $this->userFolder->getPath()); $success = $this->userFolder->nodeExists($relativePath); if ($success) { error_log("File found"); $node = $this->userFolder->get($relativePath); - $resourceInfo = $this->nodeToCS3ResourceInfo($node); + $resourceInfo = $this->utils->nodeToCS3ResourceInfo( + $node, + $this->userFolder->getPath(), + $this->userId, + $this->config->getIopIdp() + ); return new JSONResponse($resourceInfo, Http::STATUS_OK); } @@ -609,7 +490,7 @@ public function getPathByID($userId) public function initiateUpload($userId): JSONResponse { $ref = $this->request->getParam("ref"); - $path = $this->revaPathToEfssPath(($ref["path"] ?? "")); + $path = $this->utils->revaPathToEfssPath(($ref["path"] ?? "")); if ($this->userManager->userExists($userId)) { $this->init($userId); @@ -646,7 +527,7 @@ public function listFolder($userId): JSONResponse // this path is url coded, we need to decode it // for example this converts "we%20have%20space" to "we have space" $pathDecoded = urldecode(($ref["path"] ?? "")); - $path = $this->revaPathToEfssPath($pathDecoded); + $path = $this->utils->revaPathToEfssPath($pathDecoded); $success = $this->userFolder->nodeExists($path); error_log("ListFolder: $path"); @@ -659,7 +540,12 @@ public function listFolder($userId): JSONResponse $node = $this->userFolder->get($path); $nodes = $node->getDirectoryListing(); $resourceInfos = array_map(function (Node $node) { - return $this->nodeToCS3ResourceInfo($node); + return $this->utils->nodeToCS3ResourceInfo( + $node, + $this->userFolder->getPath(), + $this->userId, + $this->config->getIopIdp() + ); }, $nodes); return new JSONResponse($resourceInfos, Http::STATUS_OK); @@ -681,7 +567,7 @@ public function listGrants($userId): JSONResponse return new JSONResponse("User not found", Http::STATUS_FORBIDDEN); } - $path = $this->revaPathToEfssPath($this->request->getParam("path")); + $path = $this->utils->revaPathToEfssPath($this->request->getParam("path")); return new JSONResponse("Not implemented", Http::STATUS_NOT_IMPLEMENTED); } @@ -708,7 +594,7 @@ public function listRecycle($userId): JSONResponse foreach ($trashItems as $node) { if (preg_match("/^sciencemesh/", $node->getOriginalLocation())) { - $path = $this->efssPathToRevaPath($node->getOriginalLocation()); + $path = $this->utils->efssPathToRevaPath($node->getOriginalLocation()); $result = [ [ "opaque" => [ @@ -749,7 +635,7 @@ public function listRevisions($userId): JSONResponse return new JSONResponse("User not found", Http::STATUS_FORBIDDEN); } - $path = $this->revaPathToEfssPath($this->request->getParam("path")); + $path = $this->utils->revaPathToEfssPath($this->request->getParam("path")); return new JSONResponse("Not implemented", Http::STATUS_NOT_IMPLEMENTED); } @@ -772,7 +658,7 @@ public function removeGrant($userId): JSONResponse return new JSONResponse("User not found", Http::STATUS_FORBIDDEN); } - $path = $this->revaPathToEfssPath($this->request->getParam("path")); + $path = $this->utils->revaPathToEfssPath($this->request->getParam("path")); // FIXME: Expected a grant to remove here; return new JSONResponse("Not implemented", Http::STATUS_NOT_IMPLEMENTED); @@ -804,7 +690,7 @@ public function restoreRecycleItem($userId): JSONResponse // unique key string, see: // https://github.com/cs3org/cs3apis/blob/6eab4643f5113a54f4ce4cd8cb462685d0cdd2ef/cs3/storage/provider/v1beta1/resources.proto#L318 - if ($this->revaPathToEfssPath($key) == $node->getOriginalLocation()) { + if ($this->utils->revaPathToEfssPath($key) == $node->getOriginalLocation()) { $this->trashManager->restoreItem($node); return new JSONResponse("OK", Http::STATUS_OK); } @@ -830,7 +716,7 @@ public function restoreRevision($userId): JSONResponse return new JSONResponse("User not found", Http::STATUS_FORBIDDEN); } - $path = $this->revaPathToEfssPath($this->request->getParam("path")); + $path = $this->utils->revaPathToEfssPath($this->request->getParam("path")); // FIXME: Expected a revision param here; return new JSONResponse("Not implemented", Http::STATUS_NOT_IMPLEMENTED); @@ -852,7 +738,7 @@ public function setArbitraryMetadata($userId): JSONResponse return new JSONResponse("User not found", Http::STATUS_FORBIDDEN); } - $path = $this->revaPathToEfssPath($this->request->getParam("path")); + $path = $this->utils->revaPathToEfssPath($this->request->getParam("path")); $metadata = $this->request->getParam("metadata"); // FIXME: this needs to be implemented for real, merging the incoming metadata with the existing ones. @@ -876,7 +762,7 @@ public function unsetArbitraryMetadata($userId): JSONResponse return new JSONResponse("User not found", Http::STATUS_FORBIDDEN); } - $path = $this->revaPathToEfssPath($this->request->getParam("path")); + $path = $this->utils->revaPathToEfssPath($this->request->getParam("path")); // FIXME: this needs to be implemented for real return new JSONResponse("I'm cheating", Http::STATUS_OK); @@ -898,7 +784,7 @@ public function updateGrant($userId): JSONResponse return new JSONResponse("User not found", Http::STATUS_FORBIDDEN); } - $path = $this->revaPathToEfssPath($this->request->getParam("path")); + $path = $this->utils->revaPathToEfssPath($this->request->getParam("path")); // FIXME: Expected a parameter with the grant(s) return new JSONResponse("Not implemented", Http::STATUS_NOT_IMPLEMENTED); @@ -925,7 +811,7 @@ public function upload($userId, $path): JSONResponse } $contents = file_get_contents('php://input'); - $efssPath = $this->revaPathToEfssPath($revaPath); + $efssPath = $this->utils->revaPathToEfssPath($revaPath); error_log("Uploading! reva path: $revaPath"); error_log("Uploading! efss path $efssPath"); diff --git a/lib/Controller/UserController.php b/lib/Controller/UserController.php index 8bc62b4b..5975d85b 100644 --- a/lib/Controller/UserController.php +++ b/lib/Controller/UserController.php @@ -14,10 +14,11 @@ use Exception; use OC\Config; use OCA\ScienceMesh\ServerConfig; +use OCA\ScienceMesh\ShareProvider\ScienceMeshShareProvider; +use OCA\ScienceMesh\Utils\Utils; use OCP\AppFramework\Controller; use OCP\AppFramework\Http; use OCP\AppFramework\Http\JSONResponse; -use OCP\Files\IRootFolder; use OCP\Files\NotPermittedException; use OCP\IConfig; use OCP\IL10N; @@ -39,23 +40,28 @@ class UserController extends Controller /** @var IUserManager */ private IUserManager $userManager; + /** @var Utils */ + private Utils $utils; + /** * User Controller. * * @param string $appName * @param IRequest $request - * @param IUserManager $userManager * @param IConfig $config * @param IL10N $l10n * @param ILogger $logger + * @param IUserManager $userManager + * @param ScienceMeshShareProvider $shareProvider */ public function __construct( - string $appName, - IRequest $request, - IConfig $config, - IL10N $l10n, - ILogger $logger, - IUserManager $userManager + string $appName, + IRequest $request, + IConfig $config, + IL10N $l10n, + ILogger $logger, + IUserManager $userManager, + ScienceMeshShareProvider $shareProvider ) { parent::__construct($appName, $request); @@ -66,35 +72,7 @@ public function __construct( $this->l = $l10n; $this->logger = $logger; $this->userManager = $userManager; - } - - /** - * @throws NotPermittedException - * @throws Exception - */ - private function checkRevadAuth() - { - error_log("checkRevadAuth"); - $authHeader = $this->request->getHeader("X-Reva-Secret"); - - if ($authHeader != $this->config->getRevaSharedSecret()) { - throw new NotPermittedException("Please set an http request header 'X-Reva-Secret: '!"); - } - } - - // TODO: @Mahdi Move to utils. - private function formatUser($user): array - { - return [ - "id" => [ - "idp" => $this->config->getIopIdp(), - "opaque_id" => $user->getUID(), - ], - "display_name" => $user->getDisplayName(), - "username" => $user->getUID(), - "email" => $user->getEmailAddress(), - "type" => 1, - ]; + $this->utils = new Utils($l10n, $logger, $shareProvider); } /** @@ -104,16 +82,17 @@ private function formatUser($user): array * @NoCSRFRequired * @NoSameSiteCookieRequired * @throws NotPermittedException + * @throws Exception */ public function getUser($dummy): JSONResponse { - $this->checkRevadAuth(); + $this->utils->checkRevadAuth($this->request, $this->config->getRevaSharedSecret()); $userToCheck = $this->request->getParam("opaque_id"); if ($this->userManager->userExists($userToCheck)) { $user = $this->userManager->get($userToCheck); - $response = $this->formatUser($user); + $response = $this->utils->formatUser($user, $this->config->getIopIdp()); return new JSONResponse($response, Http::STATUS_OK); } @@ -128,10 +107,11 @@ public function getUser($dummy): JSONResponse * @NoSameSiteCookieRequired * * @throws NotPermittedException + * @throws Exception */ public function getUserByClaim($dummy): JSONResponse { - $this->checkRevadAuth(); + $this->utils->checkRevadAuth($this->request, $this->config->getRevaSharedSecret()); $userToCheck = $this->request->getParam("value"); @@ -143,7 +123,7 @@ public function getUserByClaim($dummy): JSONResponse if ($this->userManager->userExists($userToCheck)) { $user = $this->userManager->get($userToCheck); - $response = $this->formatUser($user); + $response = $this->utils->formatUser($user, $this->config->getIopIdp()); return new JSONResponse($response, Http::STATUS_OK); } diff --git a/lib/Utils/Utils.php b/lib/Utils/Utils.php new file mode 100644 index 00000000..db63400f --- /dev/null +++ b/lib/Utils/Utils.php @@ -0,0 +1,389 @@ + + */ + +namespace OCA\ScienceMesh\Utils; + +use OCA\ScienceMesh\ShareProvider\ScienceMeshShareProvider; +use OCP\Constants; +use OCP\Files\FileInfo; +use OCP\Files\InvalidPathException; +use OCP\Files\Node; +use OCP\Files\NotFoundException; +use OCP\Files\NotPermittedException; +use OCP\IL10N; +use OCP\ILogger; +use OCP\IRequest; +use OCP\IUser; +use OCP\Lock\ILockingProvider; +use OCP\Lock\LockedException; +use OCP\Share\IShare; + +/** + * ownCloud - sciencemesh + * + * This file is licensed under the MIT License. See the LICENCE file. + * @license MIT + * @copyright Sciencemesh 2020 - 2023 + * + * @author Mohammad Mahdi Baghbani Pourvahid + */ + +const RESTRICT_TO_SCIENCEMESH_FOLDER = false; +const EFSS_PREFIX = (RESTRICT_TO_SCIENCEMESH_FOLDER ? "sciencemesh/" : ""); + +// See https://github.com/pondersource/sciencemesh-php/issues/96#issuecomment-1298656896 +const REVA_PREFIX = "/home/"; + + +class Utils +{ + /** @var IL10N */ + private IL10N $l; + + /** @var ILogger */ + private ILogger $logger; + + /** @var ScienceMeshShareProvider */ + private ScienceMeshShareProvider $shareProvider; + + /** + * Utils class. + * + * @param IL10N $l10n + * @param ILogger $logger + * @param ScienceMeshShareProvider $shareProvider + */ + public function __construct( + IL10N $l10n, + ILogger $logger, + ScienceMeshShareProvider $shareProvider + ) + { + $this->l = $l10n; + $this->logger = $logger; + $this->shareProvider = $shareProvider; + } + + public function removePrefix($string, $prefix) + { + // first check if string is actually prefixed or not. + $len = strlen($prefix); + if (substr($string, 0, $len) === $prefix) { + $ret = substr($string, $len); + } else { + $ret = $string; + } + + return $ret; + } + + public function revaPathToEfssPath($revaPath): string + { + if ("$revaPath/" == REVA_PREFIX) { + error_log("revaPathToEfssPath: Interpreting special case $revaPath as ''"); + return ''; + } + $ret = EFSS_PREFIX . $this->removePrefix($revaPath, REVA_PREFIX); + error_log("revaPathToEfssPath: Interpreting $revaPath as $ret"); + return $ret; + } + + public function revaPathFromOpaqueId($opaqueId) + { + return $this->removePrefix($opaqueId, "fileid-"); + } + + public function efssPathToRevaPath($efssPath): string + { + $ret = REVA_PREFIX . $this->removePrefix($efssPath, EFSS_PREFIX); + error_log("efssPathToRevaPath: Interpreting $efssPath as $ret"); + return $ret; + } + + public function efssFullPathToRelativePath($efssFullPath, string $userFolderPath) + { + $ret = $this->removePrefix($efssFullPath, $userFolderPath); + error_log("efssFullPathToRelativePath: Interpreting $efssFullPath as $ret"); + return $ret; + } + + public function getChecksum(Node $node, $checksumType = 4): string + { + $checksumTypes = array( + 1 => "UNSET:", + 2 => "ADLER32:", + 3 => "MD5:", + 4 => "SHA1:", + ); + + // checksum is in db table oc_filecache. + // folders do not have checksum + $checksums = explode(" ", $node->getFileInfo()->getChecksum()); + + foreach ($checksums as $checksum) { + + // NOTE: that the use of !== false is deliberate (neither != false nor === true will return the desired result); + // strpos() returns either the offset at which the needle string begins in the haystack string, or the boolean + // false if the needle isn't found. Since 0 is a valid offset and 0 is "false", we can't use simpler constructs + // like !strpos($a, 'are'). + if (strpos($checksum, $checksumTypes[$checksumType]) !== false) { + return substr($checksum, strlen($checksumTypes[$checksumType])); + } + } + + return ""; + } + + /** + * @throws NotPermittedException + */ + public function checkRevadAuth(IRequest $request, string $revaSharedSecret) + { + error_log("checkRevadAuth"); + $authHeader = $request->getHeader("X-Reva-Secret"); + + if ($authHeader != $revaSharedSecret) { + throw new NotPermittedException('Please set an http request header "X-Reva-Secret: "!'); + } + } + + /** + * @param Node $node + * @return void + * + * @throws LockedException + */ + public function lock(Node $node) + { + $node->lock(ILockingProvider::LOCK_SHARED); + } + + public function getPermissionsCode(array $permissions): int + { + $permissionsCode = 0; + if (!empty($permissions["get_path"]) || !empty($permissions["get_quota"]) || !empty($permissions["initiate_file_download"]) || !empty($permissions["initiate_file_upload"]) || !empty($permissions["stat"])) { + $permissionsCode += Constants::PERMISSION_READ; + } + if (!empty($permissions["create_container"]) || !empty($permissions["move"]) || !empty($permissions["add_grant"]) || !empty($permissions["restore_file_version"]) || !empty($permissions["restore_recycle_item"])) { + $permissionsCode += Constants::PERMISSION_CREATE; + } + if (!empty($permissions["move"]) || !empty($permissions["delete"]) || !empty($permissions["remove_grant"])) { + $permissionsCode += Constants::PERMISSION_DELETE; + } + if (!empty($permissions["list_grants"]) || !empty($permissions["list_file_versions"]) || !empty($permissions["list_recycle"])) { + $permissionsCode += Constants::PERMISSION_SHARE; + } + if (!empty($permissions["update_grant"])) { + $permissionsCode += Constants::PERMISSION_UPDATE; + } + return $permissionsCode; + } + + /** + * @throws InvalidPathException + * @throws NotFoundException + */ + public function nodeToCS3ResourceInfo(Node $node, string $userFolderPath, string $userId, string $Idp): array + { + $isDirectory = ($node->getType() === FileInfo::TYPE_FOLDER); + $efssPath = substr($node->getPath(), strlen($userFolderPath) + 1); + $revaPath = $this->efssPathToRevaPath($efssPath); + + $payload = [ + "type" => ($isDirectory ? 2 : 1), + "id" => [ + "opaque_id" => "fileid-" . $revaPath, + ], + "checksum" => [ + // checksum algorithms: + // 1 UNSET + // 2 ADLER32 + // 3 MD5 + // 4 SHA1 + + // NOTE: folders do not have checksum, their type should be unset. + "type" => $isDirectory ? 1 : 4, + "sum" => $this->getChecksum($node, $isDirectory ? 1 : 4), + ], + "etag" => $node->getEtag(), + "mime_type" => ($isDirectory ? "folder" : $node->getMimetype()), + "mtime" => [ + "seconds" => $node->getMTime(), + ], + "path" => $revaPath, + "permissions" => $node->getPermissions(), + "size" => $node->getSize(), + "owner" => [ + "opaque_id" => $userId, + "idp" => $Idp, + ] + ]; + + error_log("nodeToCS3ResourceInfo " . var_export($payload, true)); + + return $payload; + } + + // For ListReceivedShares, GetReceivedShare and UpdateReceivedShare we need to include "state:2" + // see: + // https://github.com/cs3org/cs3apis/blob/cfd1ad29fdf00c79c2a321de7b1a60d0725fe4e8/cs3/sharing/ocm/v1beta1/resources.proto#L160 + /** + * @throws NotFoundException + * @throws InvalidPathException + */ + public function shareInfoToCs3Share(IShare $share, string $direction, string $token = ""): array + { + $shareId = $share->getId(); + + // TODO @Mahdi use enums! + if ($direction === "sent") { + $ocmShareData = $this->shareProvider->getSentOcmShareFromSciencemeshTable($shareId); + $ocmShareProtocols = $this->shareProvider->getSentOcmShareProtocolsFromSciencemeshTable($ocmShareData["id"]); + } elseif ($direction === "received") { + $ocmShareData = $this->shareProvider->getReceivedOcmShareFromSciencemeshTable($shareId); + $ocmShareProtocols = $this->shareProvider->getReceivedOcmShareProtocolsFromSciencemeshTable($ocmShareData["id"]); + } + + // use ocm payload stored in sciencemesh table. if it fails, use native efss share data. + // in case of total failure use "unknown". + + // this one is obvious right? + if (isset($ocmShareData["share_with"])) { + $granteeParts = explode("@", $ocmShareData["share_with"]); + } else { + $granteeParts = explode("@", $share->getSharedWith()); + } + + if (count($granteeParts) != 2) { + $granteeParts = ["unknown", "unknown"]; + } + + // the original share owner (who owns the path that is shared) + if (isset($ocmShareData["owner"])) { + $ownerParts = explode("@", $ocmShareData["owner"]); + } else { + $ownerParts = explode("@", $share->getShareOwner()); + } + + if (count($granteeParts) != 2) { + $ownerParts = ["unknown", "unknown"]; + } + + // NOTE: @Mahdi initiator/creator/sharedBy etc., whatever other names it has! means the share sharer! + // you can be owner and sharer, you can be someone who is re-sharing, in this case you are sharer but not owner + if (isset($ocmShareData["initiator"])) { + $creatorParts = explode("@", $ocmShareData["initiator"]); + } else { + $creatorParts = explode("@", $share->getSharedBy()); + } + + if (count($granteeParts) != 2) { + $creatorParts = ["unknown", "unknown"]; + } + + try { + $filePath = $share->getNode()->getPath(); + // @Mahdi why is this hardcoded? + // @Giuseppe this should be something that doesn't change when file is moved! + $opaqueId = "fileid-" . $filePath; + } catch (NotFoundException $e) { + // @Mahdi why not just return status bad request or status not found? + // @Michiel sometimes you want to translate share object even if file doesn't exist. + $opaqueId = "unknown"; + } + + // TODO: @Mahdi update this comment to point at the Reva structure mappings for this json. + // produces JSON that maps to reva + $payload = [ + // use OCM name, if null use efss share native name, if null fall back to "unknown" + "name" => $ocmShareData["name"] ?? ($share->getName() ?? "unknown"), + "token" => $token ?? "unknown", + // TODO: @Mahdi what permissions is the correct one? share permissions has different value than the share->node permissions. + // maybe use the ocmData for this one? needs testing for different scenarios to see which is the best/correct one. + "permissions" => $share->getNode()->getPermissions() ?? 0, + "id" => [ + // https://github.com/cs3org/go-cs3apis/blob/d297419/cs3/sharing/ocm/v1beta1/resources.pb.go#L423 + "opaque_id" => $shareId ?? "unknown", + ], + "resource_id" => [ + "opaque_id" => $opaqueId, + ], + // these three have been already handled and don't need "unknown" default values. + "grantee" => [ + "id" => [ + "opaque_id" => $granteeParts[0], + "idp" => $granteeParts[1], + ], + ], + "owner" => [ + "id" => [ + "opaque_id" => $ownerParts[0], + "idp" => $ownerParts[1], + ], + ], + "creator" => [ + "id" => [ + "opaque_id" => $creatorParts[0], + "idp" => $creatorParts[1], + ], + ], + // NOTE: make sure seconds type is int, otherwise Reva gives: + // error="json: cannot unmarshal string into Go struct field Timestamp.ctime.seconds of type uint64" + "ctime" => [ + "seconds" => isset($ocmShareData["ctime"]) ? (int)$ocmShareData["ctime"] : ($share->getShareTime()->getTimestamp() ?? 0) + ], + "mtime" => [ + "seconds" => isset($ocmShareData["mtime"]) ? (int)$ocmShareData["ctime"] : ($share->getShareTime()->getTimestamp() ?? 0) + ], + "access_methods" => [ + "transfer" => [ + "source_uri" => $ocmShareProtocols["transfer"]["source_uri"] ?? "unknown", + // TODO: @Mahdi this feels redundant, already included in top-level token and webdav shared_secret. + "shared_secret" => $ocmShareProtocols["transfer"]["shared_secret"] ?? "unknown", + // TODO: @Mahdi should the default value be an integer? + "size" => $ocmShareProtocols["transfer"]["size"] ?? "unknown", + ], + "webapp" => [ + "uri_template" => $ocmShareProtocols["webapp"]["uri_template"] ?? "unknown", + "view_mode" => $ocmShareProtocols["webapp"]["view_mode"] ?? "unknown", + ], + "webdav" => [ + // TODO: @Mahdi it is better to have sharedSecret and permissions in this part of code. + "uri" => $ocmShareProtocols["webdav"]["uri"] ?? "unknown", + // TODO: @Mahdi it is interesting this function accepts token as argument! is token different that the share secret? + // why do we have to pass token while the share object already has the information about token? + // $share->getToken(); + "shared_secret" => $ocmShareProtocols["webdav"]["shared_secret"] ?? "unknown", + "permissions" => $ocmShareProtocols["webdav"]["permissions"] ?? "unknown", + ], + ] + ]; + + error_log("shareInfoToCs3Share " . var_export($payload, true)); + + return $payload; + } + + public function formatUser(IUser $user, string $idp): array + { + return [ + "id" => [ + "idp" => $idp, + "opaque_id" => $user->getUID(), + ], + "display_name" => $user->getDisplayName(), + "username" => $user->getUID(), + "email" => $user->getEmailAddress(), + "type" => 1, + ]; + } +}