diff --git a/appinfo/Migrations/Version20230916.php b/appinfo/Migrations/Version20230916.php new file mode 100644 index 00000000..66b91b68 --- /dev/null +++ b/appinfo/Migrations/Version20230916.php @@ -0,0 +1,191 @@ + + */ + +namespace OCA\ScienceMesh\Migrations; + +use Doctrine\DBAL\Schema\Schema; +use OCP\Migration\ISchemaMigration; + +/** Creates initial schema */ +class Version20230916 implements ISchemaMigration +{ + public function changeSchema(Schema $schema, array $options) + { + $prefix = $options["tablePrefix"]; + + // ocm_received_shares table. + if (!$schema->hasTable("{$prefix}sciencemesh_ocm_received_shares")) { + $table = $schema->createTable("{$prefix}sciencemesh_ocm_received_shares"); + + $table->addColumn("id", "bigint", [ + "autoincrement" => true, + "unsigned" => true, + "notnull" => true, + "length" => 11, + ]); + + $table->addColumn("share_external_id", "bigint", [ + "unsigned" => false, + "notnull" => true, + "length" => 11, + ]); + + $table->addColumn("name", "string", [ + "length" => 255, + "notnull" => true, + "comment" => "Original name on the remote server" + ]); + + $table->addColumn("share_with", "string", [ + "length" => 255, + "notnull" => true, + ]); + + $table->addColumn("owner", "string", [ + "length" => 255, + "notnull" => true, + ]); + + $table->addColumn("initiator", "string", [ + "length" => 255, + "notnull" => true, + ]); + + $table->addColumn("ctime", "bigint", [ + "unsigned" => false, + "notnull" => true, + "length" => 11, + ]); + + $table->addColumn("mtime", "bigint", [ + "unsigned" => false, + "notnull" => true, + "length" => 11, + ]); + + $table->addColumn("expiration", "bigint", [ + "unsigned" => false, + "notnull" => false, + "default" => null, + "length" => 11, + ]); + + $table->addColumn("remote_share_id", "string", [ + "length" => 255, + "notnull" => false, + "default" => null, + "comment" => "share ID at the remote server" + ]); + + $table->setPrimaryKey(["id"]); + + $table->addUniqueIndex( + ["share_external_id"], + "sm_ocm_rx_ex_id_idx" + ); + $table->addUniqueIndex( + ["share_with"], + "sm_ocm_rx_sh_w_idx" + ); + } + + // ocm_protocol_transfer table. + if (!$schema->hasTable("{$prefix}sciencemesh_ocm_received_share_protocol_transfer")) { + $table = $schema->createTable("{$prefix}sciencemesh_ocm_received_share_protocol_transfer"); + + $table->addColumn("ocm_received_share_id", "bigint", [ + "unsigned" => true, + "notnull" => true, + "length" => 11, + ]); + + $table->addColumn("source_uri", "string", [ + "length" => 255, + "notnull" => true, + ]); + + $table->addColumn("shared_secret", "string", [ + "length" => 255, + "notnull" => true, + ]); + + $table->addColumn("size", "bigint", [ + "unsigned" => false, + "notnull" => true, + "length" => 11, + ]); + + $table->addUniqueIndex( + ["ocm_received_share_id"], + "sm_ocm_rx_share_id_tx_idx" + ); + } + + // ocm_protocol_webapp table. + if (!$schema->hasTable("{$prefix}sciencemesh_ocm_received_share_protocol_webapp")) { + $table = $schema->createTable("{$prefix}sciencemesh_ocm_received_share_protocol_webapp"); + + $table->addColumn("ocm_received_share_id", "bigint", [ + "unsigned" => true, + "notnull" => true, + "length" => 11, + ]); + + $table->addColumn("uri_template", "string", [ + "length" => 255, + "notnull" => true, + ]); + + $table->addColumn("view_mode", "bigint", [ + "unsigned" => false, + "notnull" => true, + "length" => 11, + ]); + + $table->addUniqueIndex( + ["ocm_received_share_id"], + "sm_ocm_rx_share_id_app_idx" + ); + } + + // ocm_protocol_webdav table. + if (!$schema->hasTable("{$prefix}sciencemesh_ocm_received_share_protocol_webdav")) { + $table = $schema->createTable("{$prefix}sciencemesh_ocm_received_share_protocol_webdav"); + + $table->addColumn("ocm_received_share_id", "bigint", [ + "unsigned" => true, + "notnull" => true, + "length" => 11, + ]); + + $table->addColumn("uri", "string", [ + "length" => 255, + "notnull" => true, + ]); + + $table->addColumn("shared_secret", "string", [ + "length" => 255, + "notnull" => true, + ]); + + $table->addColumn("permissions", "bigint", [ + "unsigned" => false, + "notnull" => true, + "length" => 11, + ]); + + $table->addUniqueIndex( + ["ocm_received_share_id"], + "sm_ocm_rx_share_id_dav_idx" + ); + } + } +} diff --git a/appinfo/Migrations/Version20230917.php b/appinfo/Migrations/Version20230917.php new file mode 100644 index 00000000..32810759 --- /dev/null +++ b/appinfo/Migrations/Version20230917.php @@ -0,0 +1,184 @@ + + */ + +namespace OCA\ScienceMesh\Migrations; + +use Doctrine\DBAL\Schema\Schema; +use OCP\Migration\ISchemaMigration; + +/** Creates initial schema */ +class Version20230917 implements ISchemaMigration +{ + public function changeSchema(Schema $schema, array $options) + { + $prefix = $options["tablePrefix"]; + + // ocm_sent_shares table. + if (!$schema->hasTable("{$prefix}sciencemesh_ocm_sent_shares")) { + $table = $schema->createTable("{$prefix}sciencemesh_ocm_sent_shares"); + + $table->addColumn("id", "bigint", [ + "autoincrement" => true, + "unsigned" => true, + "notnull" => true, + "length" => 11, + ]); + + $table->addColumn("share_internal_id", "bigint", [ + "unsigned" => false, + "notnull" => true, + "length" => 11, + ]); + + $table->addColumn("name", "string", [ + "length" => 255, + "notnull" => true, + "comment" => "Original name on the sending server" + ]); + + $table->addColumn("share_with", "string", [ + "length" => 255, + "notnull" => true, + ]); + + $table->addColumn("owner", "string", [ + "length" => 255, + "notnull" => true, + ]); + + $table->addColumn("initiator", "string", [ + "length" => 255, + "notnull" => true, + ]); + + $table->addColumn("ctime", "bigint", [ + "unsigned" => false, + "notnull" => true, + "length" => 11, + ]); + + $table->addColumn("mtime", "bigint", [ + "unsigned" => false, + "notnull" => true, + "length" => 11, + ]); + + $table->addColumn("expiration", "bigint", [ + "unsigned" => false, + "notnull" => false, + "default" => null, + "length" => 11, + ]); + + $table->setPrimaryKey(["id"]); + + $table->addUniqueIndex( + ["share_internal_id"], + "sm_ocm_tx_in_id_idx" + ); + $table->addUniqueIndex( + ["share_with"], + "sm_ocm_rx_sh_w_idx" + ); + } + + // ocm_protocol_transfer table. + if (!$schema->hasTable("{$prefix}sciencemesh_ocm_sent_share_protocol_transfer")) { + $table = $schema->createTable("{$prefix}sciencemesh_ocm_sent_share_protocol_transfer"); + + $table->addColumn("ocm_sent_share_id", "bigint", [ + "unsigned" => true, + "notnull" => true, + "length" => 11, + ]); + + $table->addColumn("source_uri", "string", [ + "length" => 255, + "notnull" => true, + ]); + + $table->addColumn("shared_secret", "string", [ + "length" => 255, + "notnull" => true, + ]); + + $table->addColumn("size", "bigint", [ + "unsigned" => false, + "notnull" => true, + "length" => 11, + ]); + + $table->addUniqueIndex( + ["ocm_sent_share_id"], + "sm_ocm_tx_share_id_tx_idx" + ); + } + + // ocm_protocol_webapp table. + if (!$schema->hasTable("{$prefix}sciencemesh_ocm_sent_share_protocol_webapp")) { + $table = $schema->createTable("{$prefix}sciencemesh_ocm_sent_share_protocol_webapp"); + + $table->addColumn("ocm_sent_share_id", "bigint", [ + "unsigned" => true, + "notnull" => true, + "length" => 11, + ]); + + $table->addColumn("uri_template", "string", [ + "length" => 255, + "notnull" => true, + ]); + + $table->addColumn("view_mode", "bigint", [ + "unsigned" => false, + "notnull" => true, + "length" => 11, + ]); + + $table->addUniqueIndex( + ["ocm_sent_share_id"], + "sm_ocm_tx_share_id_app_idx" + ); + } + + // ocm_protocol_webdav table. + if (!$schema->hasTable("{$prefix}sciencemesh_ocm_sent_share_protocol_webdav")) { + $table = $schema->createTable("{$prefix}sciencemesh_ocm_sent_share_protocol_webdav"); + + $table->addColumn("ocm_sent_share_id", "bigint", [ + "unsigned" => true, + "notnull" => true, + "length" => 11, + ]); + + $table->addColumn("uri", "string", [ + "length" => 255, + "notnull" => true, + ]); + + $table->addColumn("shared_secret", "string", [ + "length" => 255, + "notnull" => true, + ]); + + $table->addColumn("permissions", "bigint", [ + "unsigned" => false, + "notnull" => true, + "length" => 11, + ]); + + $table->addUniqueIndex( + ["ocm_sent_share_id"], + "sm_ocm_tx_share_id_dav_idx" + ); + } + } +} diff --git a/appinfo/info.xml b/appinfo/info.xml index f78cc4ed..9027ae9f 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -40,6 +40,8 @@ OCA\ScienceMesh\Sections\SciencemeshSettingsAdmin + true + ScienceMesh diff --git a/appinfo/routes.php b/appinfo/routes.php index eb840bad..7fc82375 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -37,20 +37,24 @@ ['name' => 'reva#UpdateShare', 'url' => '/~{userId}/api/ocm/UpdateShare', 'verb' => 'POST'], // TODO: @Mahdi why do we have alias here? check with @Giuseppe and Reva EFSS code. + // check in reva code, and make it to use clear names like ListSentShares and ListRxShares ['name' => 'reva#ListSentShares', 'url' => '/~{userId}/api/ocm/ListSentShares', 'verb' => 'POST'], - // alias for ListSentShares. + // alias for ListSentShares. https://github.com/cs3org/reva/blob/76d29f92b4872df37d7c3ac78f6a1574df1d320d/pkg/ocm/share/repository/nextcloud/nextcloud.go#L267 ['name' => 'reva#ListSentShares', 'url' => '/~{userId}/api/ocm/ListShares', 'verb' => 'POST'], ['name' => 'reva#ListReceivedShares', 'url' => '/~{userId}/api/ocm/ListReceivedShares', 'verb' => 'POST'], ['name' => 'reva#GetReceivedShare', 'url' => '/~{userId}/api/ocm/GetReceivedShare', 'verb' => 'POST'], ['name' => 'reva#UpdateSentShare', 'url' => '/~{userId}/api/ocm/UpdateSentShare', 'verb' => 'POST'], ['name' => 'reva#UpdateReceivedShare', 'url' => '/~{userId}/api/ocm/UpdateReceivedShare', 'verb' => 'POST'], - ['name' => 'reva#GetUser', 'url' => '/~{dummy}/api/user/GetUser', 'verb' => 'POST'], - ['name' => 'reva#GetUserByClaim', 'url' => '/~{dummy}/api/user/GetUserByClaim', 'verb' => 'POST'], + // See: https://github.com/cs3org/reva/pull/4115#discussion_r1308371946 // we need to handle this route for both nobody and userId. ['name' => 'reva#GetSentShareByToken', 'url' => '/~{userId}/api/ocm/GetSentShareByToken', 'verb' => 'POST'], + # TODO: @Mahdi move them to user controller. + ['name' => 'reva#GetUser', 'url' => '/~{dummy}/api/user/GetUser', 'verb' => 'POST'], + ['name' => 'reva#GetUserByClaim', 'url' => '/~{dummy}/api/user/GetUserByClaim', 'verb' => 'POST'], + // storage routes. ['name' => 'reva#AddGrant', 'url' => '/~{userId}/api/storage/AddGrant', 'verb' => 'POST'], ['name' => 'reva#CreateDir', 'url' => '/~{userId}/api/storage/CreateDir', 'verb' => 'POST'], @@ -77,12 +81,14 @@ ['name' => 'reva#Upload', 'url' => '/~{userId}/api/storage/Upload/{path}', 'verb' => 'PUT', 'requirements' => ['path' => '.+']], // TODO: @Mahdi Are these used anywhere in Reva? + /* // files routes. ['name' => 'storage#handleGet', 'url' => '/~{userId}/files/{path}', 'verb' => 'GET', 'requirements' => ['path' => '.+']], ['name' => 'storage#handlePost', 'url' => '/~{userId}/files/{path}', 'verb' => 'POST', 'requirements' => ['path' => '.+']], ['name' => 'storage#handlePut', 'url' => '/~{userId}/files/{path}', 'verb' => 'PUT', 'requirements' => ['path' => '.+']], ['name' => 'storage#handleDelete', 'url' => '/~{userId}/files/{path}', 'verb' => 'DELETE', 'requirements' => ['path' => '.+']], ['name' => 'storage#handleHead', 'url' => '/~{userId}/files/{path}', 'verb' => 'HEAD', 'requirements' => ['path' => '.+']], + */ // internal app routes. ['name' => 'app#contacts', 'url' => '/', 'verb' => 'GET'], diff --git a/flow.md b/flow.md new file mode 100644 index 00000000..ccf34578 --- /dev/null +++ b/flow.md @@ -0,0 +1,191 @@ +### Creating a share + + + +## Sender + +Search for contacts, it goes through the search plugin and finds all the contacts you have made. + +example output: +```json +{ + "display_name": "marie", + "idp": "revaowncloud2.docker", + "user_id": "marie", + "mail": "" +} +``` + +click on contact to create a share. this step appends postfix for sm shares to distinguish them +from regular federated shares. + +Call share provider `create`, this is called by share manager, share manager calls share provider factory which has +been overridden to be sm share provider. + +sm share provider will route sm share and regular federated shares based on the appended postfix. + +sm share goes to: revaHttpClient->createShare + +example request: +```json +{ + "sourcePath": "\\/home\\/test\\/", + "targetPath": "\\/test\\/", + "type": "dir", + "recipientUsername": "marie", + "recipientHost": "revaowncloud2.docker", + "role": "viewer" +} +``` + +reva received the request and does these calls in order: +1. "POST /index.php/apps/sciencemesh/~einstein/api/auth/Authenticate HTTP/1.1" +2. "POST /index.php/apps/sciencemesh/~einstein/api/storage/CreateHome HTTP/1.1" +3. "POST /index.php/apps/sciencemesh/~einstein/api/storage/GetMD HTTP/1.1" 200 +sm response: +```php +array ( + 'type' => 2, + 'id' => + array ( + 'opaque_id' => 'fileid-/home/test', + ), + 'checksum' => + array ( + 'type' => 1, + 'sum' => '', + ), + 'etag' => '65129328493b0', + 'mime_type' => 'folder', + 'mtime' => + array ( + 'seconds' => 1695716136, + ), + 'path' => '/home/test', + 'permissions' => 31, + 'size' => 0, + 'owner' => + array ( + 'opaque_id' => 'einstein', + 'idp' => 'revaowncloud1.docker', + ), +) +``` +4. "POST /index.php/apps/sciencemesh/~einstein/api/storage/GetMD HTTP/1.1" 200 +sm response: +```php +array ( + 'type' => 2, + 'id' => + array ( + 'opaque_id' => 'fileid-/home/test', + ), + 'checksum' => + array ( + 'type' => 1, + 'sum' => '', + ), + 'etag' => '65129328493b0', + 'mime_type' => 'folder', + 'mtime' => + array ( + 'seconds' => 1695716136, + ), + 'path' => '/home/test', + 'permissions' => 31, + 'size' => 0, + 'owner' => + array ( + 'opaque_id' => 'einstein', + 'idp' => 'revaowncloud1.docker', + ), +) +``` +5. "POST /index.php/apps/sciencemesh/~einstein/api/ocm/addSentShare HTTP/1.1" 201 +reva payload: +```php +array ( + 'userId' => 'einstein', + '_route' => 'sciencemesh.reva.addSentShare', + 'resourceId' => + array ( + 'storageId' => 'nextcloud', + 'opaqueId' => 'fileid-/home/test', + ), + 'name' => 'test', + 'token' => 'Y7bWUulmHrhfUJ8LRknNpkZQGcRRkMk7', + 'grantee' => + array ( + 'type' => 'GRANTEE_TYPE_USER', + 'userId' => + array ( + 'idp' => 'revaowncloud2.docker', + 'opaqueId' => 'marie', + ), + ), + 'owner' => + array ( + 'idp' => 'revaowncloud1.docker', + 'opaqueId' => 'einstein', + 'type' => 'USER_TYPE_PRIMARY', + ), + 'creator' => + array ( + 'idp' => 'revaowncloud1.docker', + 'opaqueId' => 'einstein', + ), + 'ctime' => + array ( + 'seconds' => '1695716163', + 'nanos' => 943856286, + ), + 'mtime' => + array ( + 'seconds' => '1695716163', + 'nanos' => 943856286, + ), + 'shareType' => 'SHARE_TYPE_USER', + 'accessMethods' => + array ( + 0 => + array ( + 'webdavOptions' => + array ( + 'permissions' => + array ( + 'getPath' => true, + 'getQuota' => true, + 'initiateFileDownload' => true, + 'listGrants' => true, + 'listContainer' => true, + 'listFileVersions' => true, + 'listRecycle' => true, + 'stat' => true, + ), + ), + ), + 1 => + array ( + 'webappOptions' => + array ( + 'viewMode' => 'VIEW_MODE_READ_ONLY', + ), + ), + ), +) +``` + +sm app creates share object via sm share provider `createInternal` that inserts share into efss native db. + +probably owncloud would inform the recepient by these request (should dig deeper to be sure): +"POST /ocs/v2.php/apps/files_sharing/api/v1/shares?format=json HTTP/1.1" 200 +"GET /ocs/v2.php/apps/files_sharing/api/v1/shares?format=json&path=%2Ftest&reshares=true + + +## Receiver + +Reva will call these in order: +1. "POST /index.php/apps/sciencemesh/~unauthenticated/api/user/GetUser HTTP/1.1" 200 +2. "POST /index.php/apps/sciencemesh/~marie/api/ocm/addReceivedShare HTTP/1.1" 400 + +FIX: 400 in addReceivedShare diff --git a/lib/Controller/RevaController.php b/lib/Controller/RevaController.php index 3570ce22..c37ce673 100644 --- a/lib/Controller/RevaController.php +++ b/lib/Controller/RevaController.php @@ -34,6 +34,8 @@ use OCP\Files\NotFoundException; use OCP\Files\NotPermittedException; use OCP\IConfig; +use OCP\IL10N; +use OCP\ILogger; use OCP\IRequest; use OCP\IURLGenerator; use OCP\IUserManager; @@ -45,11 +47,11 @@ use OCP\Share\IShare; use Symfony\Component\Filesystem\Exception\FileNotFoundException; -define('RESTRICT_TO_SCIENCEMESH_FOLDER', false); -define('EFSS_PREFIX', (RESTRICT_TO_SCIENCEMESH_FOLDER ? 'sciencemesh/' : '')); +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/'); +define("REVA_PREFIX", "/home/"); class RevaController extends Controller { @@ -80,6 +82,27 @@ class RevaController extends Controller /** @var Folder */ private Folder $userFolder; + /** @var IL10N */ + private IL10N $l; + + /** @var ILogger */ + private ILogger $logger; + + /** + * Reva Controller. + * + * @param string $appName + * @param IRootFolder $rootFolder + * @param IRequest $request + * @param IUserManager $userManager + * @param IURLGenerator $urlGenerator + * @param IConfig $config + * @param TrashBinManager $trashManager + * @param IManager $shareManager + * @param IL10N $l10n + * @param ILogger $logger + * @param ScienceMeshShareProvider $shareProvider + */ public function __construct( string $appName, IRootFolder $rootFolder, @@ -89,11 +112,13 @@ public function __construct( IConfig $config, TrashBinManager $trashManager, IManager $shareManager, + IL10N $l10n, + ILogger $logger, ScienceMeshShareProvider $shareProvider ) { parent::__construct($appName, $request); - require_once(__DIR__ . '/../../vendor/autoload.php'); + require_once(__DIR__ . "/../../vendor/autoload.php"); $this->rootFolder = $rootFolder; $this->request = $request; @@ -102,6 +127,8 @@ public function __construct( $this->config = new ServerConfig($config); $this->trashManager = $trashManager; $this->shareManager = $shareManager; + $this->l = $l10n; + $this->logger = $logger; $this->shareProvider = $shareProvider; } @@ -194,40 +221,43 @@ private function removePrefix($string, $prefix) * @return JSONResponse * @throws NotPermittedException * @throws ShareNotFound|IllegalIDChangeException + * @throws Exception */ public function Authenticate($userId): JSONResponse { - error_log("Authenticate"); + error_log("Authenticate: " . $userId); + if ($this->userManager->userExists($userId)) { $this->init($userId); - } else { - $share = $this->shareProvider->getSentShareByToken($userId); - if ($share) { - $sharedWith = explode("@", $share->getSharedWith()); - $result = [ - "user" => $this->formatFederatedUser($sharedWith[0], $sharedWith[1]), - "scopes" => [], - ]; - return new JSONResponse($result, Http::STATUS_OK); - } else { - return new JSONResponse("User not found", Http::STATUS_FORBIDDEN); - } - } - $userId = $this->request->getParam("clientID"); - $password = $this->request->getParam("clientSecret"); + $userId = $this->request->getParam("clientID"); + $password = $this->request->getParam("clientSecret"); + // Try e.g.: + // curl -v -H 'Content-Type:application/json' -d'{"clientID":"einstein",clientSecret":"relativity"}' http://einstein:relativity@localhost/index.php/apps/sciencemesh/~einstein/api/auth/Authenticate - // Try e.g.: - // curl -v -H 'Content-Type:application/json' -d'{"clientID":"einstein",clientSecret":"relativity"}' http://einstein:relativity@localhost/index.php/apps/sciencemesh/~einstein/api/auth/Authenticate + // see: https://github.com/cs3org/reva/issues/2356 + if ($password == $this->config->getRevaLoopbackSecret()) { + // NOTE: @Mahdi, usually everything goes in this branch. + $user = $this->userManager->get($userId); + } else { + $user = $this->userManager->checkPassword($userId, $password); + } - // see: https://github.com/cs3org/reva/issues/2356 - if ($password == $this->config->getRevaLoopbackSecret()) { - $user = $this->userManager->get($userId); } else { - $user = $this->userManager->checkPassword($userId, $password); + $share = $this->shareProvider->getSentShareByToken($userId); + $userId = $share->getSharedBy(); + $user = $this->userManager->get($userId); } + if ($user) { - // FIXME this hardcoded value represents {"resource_id":{"storage_id":"storage-id","opaque_id":"opaque-id"},"path":"some/file/path.txt"} and is not needed + // FIXME: @Mahdi this hardcoded value represents the json below and is not needed. + // { + // "resource_id": { + // "storage_id": "storage-id", + // "opaque_id": "opaque-id" + // }, + // "path": "some/file/path.txt" + // } $result = [ "user" => $this->formatUser($user), "scopes" => [ @@ -240,14 +270,16 @@ public function Authenticate($userId): JSONResponse ], ], ]; + return new JSONResponse($result, Http::STATUS_OK); } - return new JSONResponse("Username / password not recognized", 401); + return new JSONResponse("Username / password not recognized", Http::STATUS_UNAUTHORIZED); } /** * GetSentShareByToken gets the information for a share by the given token. + * * @PublicPage * @NoCSRFRequired * @param $userId @@ -263,7 +295,7 @@ public function GetSentShareByToken($userId): JSONResponse { error_log("GetSentShareByToken: user is -> $userId"); - // See: https://github.com/cs3org/reva/pull/4115#discussion_r1308371946 + // see: https://github.com/cs3org/reva/pull/4115#discussion_r1308371946 if ($userId !== "nobody") { if ($this->userManager->userExists($userId)) { $this->init($userId); @@ -275,79 +307,150 @@ public function GetSentShareByToken($userId): JSONResponse $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); if ($share) { - $response = $this->shareInfoToCs3Share($share, $token); + $response = $this->shareInfoToCs3Share($share, "sent", $token); return new JSONResponse($response, Http::STATUS_OK); } return new JSONResponse(["error" => "GetSentShare failed"], Http::STATUS_BAD_REQUEST); } + // 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, $token = ''): array + private function shareInfoToCs3Share(IShare $share, string $direction, $token = ""): array { - $shareeParts = explode("@", $share->getSharedWith()); - if (count($shareeParts) == 1) { - error_log("warning, could not find sharee user@host from '" . $share->getSharedWith() . "'"); - $shareeParts = ["unknown", "unknown"]; + $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"]; } - $ownerParts = [$share->getShareOwner(), $this->getDomainFromURL($this->config->getIopUrl())]; + // 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()); + } - $stime = 0; // $share->getShareTime()->getTimeStamp(); + 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"; } - // produces JSON that maps to - // https://github.com/cs3org/reva/blob/v1.18.0/pkg/ocm/share/manager/nextcloud/nextcloud.go#L77 - // and - // https://github.com/cs3org/go-cs3apis/blob/d297419/cs3/sharing/ocm/v1beta1/resources.pb.go#L100 + // 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" => $share->getId() + "opaque_id" => $shareId ?? "unknown", ], "resource_id" => [ - "opaque_id" => $opaqueId + "opaque_id" => $opaqueId, ], - "permissions" => $share->getNode()->getPermissions(), - // https://github.com/cs3org/go-cs3apis/blob/d29741980082ecd0f70fe10bd2e98cf75764e858/cs3/storage/provider/v1beta1/resources.pb.go#L897 + // these three have been already handled and don't need "unknown" default values. "grantee" => [ - "type" => 1, // https://github.com/cs3org/go-cs3apis/blob/d29741980082ecd0f70fe10bd2e98cf75764e858/cs3/storage/provider/v1beta1/resources.pb.go#L135 "id" => [ - "opaque_id" => $shareeParts[0], - "idp" => $shareeParts[1] + "opaque_id" => $granteeParts[0], + "idp" => $granteeParts[1], ], ], "owner" => [ "id" => [ "opaque_id" => $ownerParts[0], - "idp" => $ownerParts[1] + "idp" => $ownerParts[1], ], ], "creator" => [ "id" => [ - "opaque_id" => $ownerParts[0], - "idp" => $ownerParts[1] + "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" => $stime + "seconds" => isset($ocmShareData["ctime"]) ? (int)$ocmShareData["ctime"] : ($share->getShareTime()->getTimestamp() ?? 0) ], "mtime" => [ - "seconds" => $stime + "seconds" => isset($ocmShareData["mtime"]) ? (int)$ocmShareData["ctime"] : ($share->getShareTime()->getTimestamp() ?? 0) ], - "token" => $token + "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)); @@ -355,32 +458,20 @@ private function shareInfoToCs3Share(IShare $share, $token = ''): array return $payload; } + // TODO: @Mahdi Move to utils. private function getDomainFromURL($url) { // converts https://revaowncloud1.docker/ to revaowncloud1.docker - // Note: do not use it on anything without http(s) in the start, it would return null. + // NOTE: do not use it on anything without http(s) in the start, it would return null. return str_ireplace("www.", "", parse_url($url, PHP_URL_HOST)); } - private function formatFederatedUser($username, $remote): array - { - return [ - "id" => [ - "idp" => $remote, - "opaque_id" => $username, - ], - "display_name" => $username, // FIXME: this comes in the OCM share payload - "username" => $username, - "email" => "unknown@unknown", // FIXME: this comes in the OCM share payload - "type" => 6, - ]; - } - + // TODO: @Mahdi Move to utils. private function formatUser($user): array { return [ "id" => [ - "idp" => $this->getDomainFromURL($this->config->getIopUrl()), + "idp" => $this->config->getIopIdp(), "opaque_id" => $user->getUID(), ], "display_name" => $user->getDisplayName(), @@ -418,9 +509,6 @@ public function CreateDir($userId): JSONResponse return new JSONResponse("OK", Http::STATUS_OK); } - // TODO: @Mahdi What does this even mean? what is state:2 ? - // For ListReceivedShares, GetReceivedShare and UpdateReceivedShare we need to include "state:2" - /** * @PublicPage * @NoAdminRequired @@ -451,9 +539,6 @@ public function CreateHome($userId): JSONResponse return new JSONResponse("OK", Http::STATUS_OK); } - // TODO: @Mahdi what is this? - // corresponds the permissions we got from Reva to Nextcloud - /** * @PublicPage * @NoAdminRequired @@ -474,6 +559,8 @@ public function CreateReference($userId): JSONResponse return new JSONResponse("Not implemented", Http::STATUS_NOT_IMPLEMENTED); } + // TODO: @Mahdi maybe not used anymore. + /** * @PublicPage * @NoAdminRequired @@ -568,7 +655,7 @@ public function Delete($userId): JSONResponse */ public function EmptyRecycle($userId): JSONResponse { - // DIFFERENT FUNCTION IN NC/OC + // TODO: @Mahdi fix this! DIFFERENT FUNCTION IN NC/OC error_log("EmptyRecycle"); if ($this->userManager->userExists($userId)) { $this->init($userId); @@ -602,14 +689,37 @@ public function GetMD($userId): JSONResponse $ref = $this->request->getParam("ref"); error_log("GetMD " . var_export($ref, true)); + if (!isset($ref)) { + return new JSONResponse("ref not found in the request.", Http::STATUS_BAD_REQUEST); + } + if (isset($ref["path"])) { - // e.g. GetMD {"ref": {"path": "/home/asdf"}, "mdKeys": null} + // e.g. GetMD: + // { + // "ref": { + // "path": "/home/asdf" + // }, + // "mdKeys": null + // } $revaPath = $ref["path"]; - } else if (isset($ref["resource_id"]) && isset($ref["resource_id"]["opaque_id"]) && str_starts_with($ref["resource_id"]["opaque_id"], "fileid-")) { - // e.g. GetMD {"ref": {"resource_id": {"storage_id": "00000000-0000-0000-0000-000000000000", "opaque_id": "fileid-/asdf"}}, "mdKeys":null} + } else if ( + isset($ref["resource_id"]["opaque_id"]) + && + str_starts_with($ref["resource_id"]["opaque_id"], "fileid-") + ) { + // e.g. GetMD: + // { + // "ref": { + // "resource_id": { + // "storage_id": "00000000-0000-0000-0000-000000000000", + // "opaque_id": "fileid-/asdf" + // } + // }, + // "mdKeys": null + // } $revaPath = $this->revaPathFromOpaqueId($ref["resource_id"]["opaque_id"]); } else { - throw new Exception("ref not understood!"); + return new JSONResponse("ref not understood!", Http::STATUS_BAD_REQUEST); } // this path is url coded, we need to decode it @@ -671,7 +781,7 @@ private function nodeToCS3ResourceInfo(Node $node): array // 3 MD5 // 4 SHA1 - // note: folders do not have checksum, their type should be unset. + // NOTE: folders do not have checksum, their type should be unset. "type" => $isDirectory ? 1 : 4, "sum" => $this->getChecksum($node, $isDirectory ? 1 : 4), ], @@ -712,11 +822,11 @@ private function getChecksum(Node $node, $checksumType = 4): string // checksum is in db table oc_filecache. // folders do not have checksum - $checksums = explode(' ', $node->getFileInfo()->getChecksum()); + $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); + // 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'). @@ -725,9 +835,11 @@ private function getChecksum(Node $node, $checksumType = 4): string } } - return ''; + return ""; } + // TODO: @Mahdi remove. + /** * @PublicPage * @NoAdminRequired @@ -743,6 +855,7 @@ public function GetPathByID($userId) return new JSONResponse("User not found", Http::STATUS_FORBIDDEN); } + // TODO: @Mahdi what is "in progress"? what should be done here? // in progress $path = "subdir/"; $storageId = $this->request->getParam("storage_id"); @@ -1144,6 +1257,7 @@ public function Upload($userId, $path): JSONResponse /** * Get user list. + * * @PublicPage * @NoCSRFRequired * @NoSameSiteCookieRequired @@ -1169,6 +1283,7 @@ public function GetUser($dummy): JSONResponse /** * Get user by claim. + * * @PublicPage * @NoCSRFRequired * @NoSameSiteCookieRequired @@ -1201,6 +1316,7 @@ public function GetUserByClaim($dummy): JSONResponse /** * Create a new share in fn with the given access control list. + * * @PublicPage * @NoCSRFRequired * @return Http\DataResponse|JSONResponse @@ -1220,56 +1336,214 @@ public function addSentShare($userId) $params = $this->request->getParams(); error_log("addSentShare " . var_export($params, true)); - $owner = $params["owner"]["opaqueId"]; // . "@" . $params["owner"]["idp"]; - $name = $params["name"]; // "fileid-/other/q/f gr" - $resourceOpaqueId = $params["resourceId"]["opaqueId"]; // "fileid-/other/q/f gr" - $revaPath = $this->revaPathFromOpaqueId($resourceOpaqueId); // "/other/q/f gr" + $name = $params["name"] ?? null; + $token = $params["token"] ?? null; + $ctime = (int)$params["ctime"]["seconds"] ?? null; + $mtime = (int)$params["mtime"]["seconds"] ?? null; + $resourceId = $params["resourceId"]["opaqueId"] ?? null; + $payloadUserId = $params["userId"] ?? null; + + if (!isset($name)) { + return new JSONResponse("name not found in the request.", Http::STATUS_BAD_REQUEST); + } + if (!isset($token)) { + return new JSONResponse("token not found in the request.", Http::STATUS_BAD_REQUEST); + } + if (!isset($ctime)) { + return new JSONResponse("ctime not found in the request.", Http::STATUS_BAD_REQUEST); + } + if (!isset($mtime)) { + return new JSONResponse("mtime not found in the request.", Http::STATUS_BAD_REQUEST); + } + if (!isset($resourceId)) { + return new JSONResponse("resourceId->opaqueId not found in the request.", Http::STATUS_BAD_REQUEST); + } + if (!isset($payloadUserId)) { + return new JSONResponse("userId not found in the request.", Http::STATUS_BAD_REQUEST); + } + + // 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); - $revaPermissions = null; + try { + $node = $this->userFolder->get($efssPath); + } catch (NotFoundException $e) { + return new JSONResponse("share failed. resource path not found.", Http::STATUS_BAD_REQUEST); + } - foreach ($params['accessMethods'] as $accessMethod) { - if (isset($accessMethod['webdavOptions'])) { - $revaPermissions = $accessMethod['webdavOptions']['permissions']; - break; - } + if (isset($params["grantee"]["userId"])) { + $granteeIdp = $params["grantee"]["userId"]["idp"] ?? null; + $granteeOpaqueId = $params["grantee"]["userId"]["opaqueId"] ?? null; + } + + if (isset($params["owner"])) { + $ownerIdp = $params["owner"]["idp"] ?? null; + $ownerOpaqueId = $params["owner"]["opaqueId"] ?? null; + } + + if (isset($params["creator"])) { + $creatorIdp = $params["creator"]["idp"] ?? null; + $creatorOpaqueId = $params["creator"]["opaqueId"] ?? null; + } + + if (!isset($granteeIdp)) { + return new JSONResponse("grantee idp not found in the request.", Http::STATUS_BAD_REQUEST); + } + if (!isset($granteeOpaqueId)) { + return new JSONResponse("grantee opaqueId not found in the request.", Http::STATUS_BAD_REQUEST); + } + if (!isset($ownerIdp)) { + return new JSONResponse("owner idp not found in the request.", Http::STATUS_BAD_REQUEST); + } + if (!isset($ownerOpaqueId)) { + return new JSONResponse("owner opaqueId not found in the request.", Http::STATUS_BAD_REQUEST); + } + if (!isset($creatorIdp)) { + return new JSONResponse("creator idp not found in the request.", Http::STATUS_BAD_REQUEST); + } + if (!isset($creatorOpaqueId)) { + return new JSONResponse("creator opaqueId not found in the request.", Http::STATUS_BAD_REQUEST); } - if (!isset($revaPermissions)) { - throw new Exception('reva permissions not found'); + // NOTE: @Mahdi this 3 variables should be exactly same as of now. + // maybe it would be subject to change in future but for now it is good to check these + // instead of blindly assuming they're same. + if ($userId !== $payloadUserId || $userId !== $creatorOpaqueId || $payloadUserId !== $creatorOpaqueId) { + return new JSONResponse( + "creator->opaqueId, userId and userId from the payload are mismatched.", + Http::STATUS_UNPROCESSABLE_ENTITY + ); } - $granteeType = $params["grantee"]["type"]; // "GRANTEE_TYPE_USER" - $granteeHost = $params["grantee"]["userId"]["idp"]; // "revanc2.docker" - $granteeUser = $params["grantee"]["userId"]["opaqueId"]; // "marie" + // don't allow ScienceMesh shares if source and target server are the same. + // this means users with the same reva iop cannot share with each other via sciencemesh and + // should use their native efss capabilities to do so. + // see: https://github.com/sciencemesh/nc-sciencemesh/issues/57 + if ($ownerIdp === $granteeIdp) { + $message = "Not allowed to create a ScienceMesh share for a user on the same server %s as sender %s."; + $this->logger->debug( + sprintf( + $message, $ownerIdp, $granteeIdp + ), + ["app" => "sciencemesh"] + ); + return new JSONResponse( + "Not allowed to create a ScienceMesh share for a user on the same server %s as sender %s.", + Http::STATUS_UNPROCESSABLE_ENTITY + ); + } - $efssPermissions = $this->getPermissionsCode($revaPermissions); - $shareWith = $granteeUser . "@" . $granteeHost; - $sharedSecret = $params["token"]; + if (!isset($params["accessMethods"])) { + return new JSONResponse("accessMethods not found in the request.", Http::STATUS_BAD_REQUEST); + } - try { - $node = $this->userFolder->get($efssPath); - } catch (NotFoundException $e) { - return new JSONResponse(["error" => "Share failed. Resource Path not found"], Http::STATUS_BAD_REQUEST); + $accessMethods = $params["accessMethods"]; + + // TODO: @Mahdi these one has problems, check and debug. + foreach ($accessMethods as $method) { + if (isset($method["transferOptions"])) { + $ocmProtocolTransfer = $method["transferOptions"]; + } + if (isset($method["webappOptions"])) { + $ocmProtocolWebapp = $method["webappOptions"]; + } + if (isset($method["webdavOptions"])) { + $ocmProtocolWebdav = $method["webdavOptions"]; + } } - error_log("calling newShare"); + if (!isset($ocmProtocolWebdav)) { + return new JSONResponse("webdavOptions not found in the request.", Http::STATUS_BAD_REQUEST); + } + + // handle bad cases and eliminate null variables. + if (!isset($ocmProtocolWebdav["permissions"])) { + return new JSONResponse("webdavOptions permissions not found in the request.", Http::STATUS_BAD_REQUEST); + } + + // convert permissions from array to integer. + $permissions = $this->getPermissionsCode($ocmProtocolWebdav["permissions"]); + + // prepare data for adding to the native efss table. $share = $this->shareManager->newShare(); $share->setNode($node); + $share->setShareType(ScienceMeshApp::SHARE_TYPE_SCIENCEMESH); + $share->setSharedWith($granteeOpaqueId . "@" . $granteeIdp); + $share->setShareOwner($ownerOpaqueId); + $share->setSharedBy($creatorOpaqueId); + $share->setPermissions($permissions); + $share->setToken($token); + $share->setShareTime(new DateTime("@$ctime")); + + // check if file is not already shared with the remote user + $alreadyShared = $this->shareProvider->getSharedWith( + $share->getSharedWith(), + $share->getShareType(), + $share->getNode(), + 1, + 0 + ); + + if (!empty($alreadyShared)) { + $message = "Sharing %s failed, because this item is already shared with %s"; + $message_t = $this->l->t( + "Sharing %s failed, because this item is already shared with %s", + [$share->getNode()->getName(), $share->getSharedWith()] + ); + $this->logger->debug( + sprintf( + $message, $share->getNode()->getName(), $share->getSharedWith() + ), + ["app" => "sciencemesh"] + ); + return new JSONResponse($message_t, Http::STATUS_CONFLICT); + } + + // ScienceMesh shares always have read permissions + if (($share->getPermissions() & Constants::PERMISSION_READ) === 0) { + $message = 'ScienceMesh shares require read permissions'; + $message_t = $this->l->t('ScienceMesh shares require read permissions'); + $this->logger->debug($message, ['app' => 'ScienceMesh']); + return new JSONResponse($message_t, Http::STATUS_UNPROCESSABLE_ENTITY); + } + $this->lock($share->getNode()); - $share->setShareType(ScienceMeshApp::SHARE_TYPE_SCIENCEMESH); - $share->setSharedBy($userId); - $share->setSharedWith($shareWith); - $share->setShareOwner($owner); - $share->setPermissions($efssPermissions); - $share->setToken($sharedSecret); - $share = $this->shareProvider->createInternal($share); - - return new DataResponse($share->getId(), Http::STATUS_CREATED); + // prepare share data for ocm + $share = $this->shareProvider->createNativeEfssScienceMeshShare($share); + $efssShareInternalId = $share->getId(); + + // prepare data for adding to the ScienceMesh OCM table. + // see: https://github.com/sciencemesh/nc-sciencemesh/issues/45 + + $expiration = $params["expiration"] ?? null; + if (isset($expiration)) { + $expiration = (int)$expiration; + } + + $ocmShareData = [ + "share_internal_id" => $efssShareInternalId, + "name" => $name, + "share_with" => $granteeOpaqueId . "@" . $granteeIdp, + "owner" => $ownerOpaqueId . "@" . $ownerIdp, + "initiator" => $creatorOpaqueId . "@" . $creatorIdp, + "ctime" => $ctime, + "mtime" => $mtime, + "expiration" => $expiration, + "transfer" => $ocmProtocolTransfer ?? null, + "webapp" => $ocmProtocolWebapp ?? null, + "webdav" => $ocmProtocolWebdav, + ]; + + $this->shareProvider->addSentOcmShareToSciencemeshTable($ocmShareData); + + return new DataResponse($efssShareInternalId, Http::STATUS_CREATED); } + // TODO: @Mahdi Move to utils. private function getPermissionsCode(array $permissions): int { $permissionsCode = 0; @@ -1291,6 +1565,8 @@ private function getPermissionsCode(array $permissions): int return $permissionsCode; } + // TODO: @Mahdi Move to utils. + /** * @param Node $node * @return void @@ -1300,7 +1576,6 @@ private function getPermissionsCode(array $permissions): int private function lock(Node $node) { $node->lock(ILockingProvider::LOCK_SHARED); - $this->lockedNode = $node; } /** @@ -1315,53 +1590,171 @@ private function lock(Node $node) */ public function addReceivedShare($userId): JSONResponse { + if ($this->userManager->userExists($userId)) { + $this->init($userId); + } else { + return new JSONResponse("User not found", Http::STATUS_FORBIDDEN); + } + $params = $this->request->getParams(); error_log("addReceivedShare " . var_export($params, true)); - foreach ($params['protocols'] as $protocol) { - if (isset($protocol['webdavOptions'])) { - $sharedSecret = $protocol['webdavOptions']['sharedSecret']; - // make sure you have webdav_endpoint = "https://nc1.docker/" under - // [grpc.services.ocmshareprovider] in the sending Reva's config - $uri = $protocol['webdavOptions']['uri']; // e.g. https://nc1.docker/remote.php/dav/ocm/vaKE36Wf1lJWCvpDcRQUScraVP5quhzA - $remote = implode('/', array_slice(explode('/', $uri), 0, 3)); // e.g. https://nc1.docker - break; - } + + $name = $params["name"] ?? null; + $ctime = (int)$params["ctime"]["seconds"] ?? null; + $mtime = (int)$params["mtime"]["seconds"] ?? null; + $remoteShareId = $params["remoteShareId"] ?? null; + + if (!isset($name)) { + return new JSONResponse("name not found in the request.", Http::STATUS_BAD_REQUEST); + } + if (!isset($ctime)) { + return new JSONResponse("ctime not found in the request.", Http::STATUS_BAD_REQUEST); + } + if (!isset($mtime)) { + return new JSONResponse("mtime not found in the request.", Http::STATUS_BAD_REQUEST); + } + if (!isset($remoteShareId)) { + return new JSONResponse("remoteShareId not found in the request.", Http::STATUS_BAD_REQUEST); } - if (!isset($sharedSecret)) { - throw new Exception('sharedSecret not found'); + if (isset($params["grantee"]["userId"])) { + $granteeIdp = $params["grantee"]["userId"]["idp"] ?? null; + $granteeOpaqueId = $params["grantee"]["userId"]["opaqueId"] ?? null; } - if (!isset($remote)) { - throw new Exception('protocols[[webdavOptions][uri]] not found'); + if (isset($params["owner"])) { + $ownerIdp = $params["owner"]["idp"] ?? null; + $ownerOpaqueId = $params["owner"]["opaqueId"] ?? null; } - $shareData = [ - "remote" => $remote, //https://nc1.docker - "remote_id" => $params["remoteShareId"], // the id of the share in the oc_share table of the remote. - "share_token" => $sharedSecret, // 'tDPRTrLI4hE3C5T' - "password" => "", - "name" => rtrim($params["name"], "/"), // '/grfe' - "owner" => $params["owner"]["opaqueId"], // 'einstein' - "user" => $userId // 'marie' - ]; + if (isset($params["creator"])) { + $creatorIdp = $params["creator"]["idp"] ?? null; + $creatorOpaqueId = $params["creator"]["opaqueId"] ?? null; + } - if ($this->userManager->userExists($userId)) { - $this->init($userId); - } else { - return new JSONResponse("User not found", Http::STATUS_FORBIDDEN); + if (!isset($granteeIdp)) { + return new JSONResponse("grantee idp not found in the request.", Http::STATUS_BAD_REQUEST); + } + if (!isset($granteeOpaqueId)) { + return new JSONResponse("grantee opaqueId not found in the request.", Http::STATUS_BAD_REQUEST); + } + if (!isset($ownerIdp)) { + return new JSONResponse("owner idp not found in the request.", Http::STATUS_BAD_REQUEST); + } + if (!isset($ownerOpaqueId)) { + return new JSONResponse("owner opaqueId not found in the request.", Http::STATUS_BAD_REQUEST); + } + if (!isset($creatorIdp)) { + return new JSONResponse("creator idp not found in the request.", Http::STATUS_BAD_REQUEST); + } + if (!isset($creatorOpaqueId)) { + return new JSONResponse("creator opaqueId not found in the request.", Http::STATUS_BAD_REQUEST); + } + + if (!isset($params["protocols"])) { + return new JSONResponse("protocols not found in the request.", Http::STATUS_BAD_REQUEST); + } + + $protocols = $params["protocols"]; + + foreach ($protocols as $protocol) { + if (isset($protocol["transferOptions"])) { + $ocmProtocolTransfer = $protocol["transferOptions"]; + } + if (isset($protocol["webappOptions"])) { + $ocmProtocolWebapp = $protocol["webappOptions"]; + } + if (isset($protocol["webdavOptions"])) { + $ocmProtocolWebdav = $protocol["webdavOptions"]; + } + } + + if (!isset($ocmProtocolWebdav)) { + return new JSONResponse("webdavOptions not found in the request.", Http::STATUS_BAD_REQUEST); + } + + // handle bad cases and eliminate null variables. + if (!isset($ocmProtocolWebdav["permissions"]["permissions"])) { + return new JSONResponse("webdavOptions permissions not found in the request.", Http::STATUS_BAD_REQUEST); + } + if (!isset($ocmProtocolWebdav["sharedSecret"])) { + return new JSONResponse("webdavOptions sharedSecret not found in the request.", Http::STATUS_BAD_REQUEST); + } + if (!isset($ocmProtocolWebdav["uri"])) { + return new JSONResponse("webdavOptions uri not found in the request.", Http::STATUS_BAD_REQUEST); } - $scienceMeshData = [ - "is_external" => true, + // convert permissions from array to integer. + $integerPermissions = $this->getPermissionsCode($ocmProtocolWebdav["permissions"]["permissions"]); + $ocmProtocolWebdav["permissions"] = $integerPermissions; + + $sharedSecret = $ocmProtocolWebdav["sharedSecret"]; + // URI example: https://nc1.docker/remote.php/dav/ocm/vaKE36Wf1lJWCvpDcRQUScraVP5quhzA + $uri = $ocmProtocolWebdav["uri"]; + // remote extracted from URI: https://nc1.docker + // below line splits uri by the "/" character, picks first 3 items aka ["https:", "", "nc.docker"] + // and joins them again with "/" character in between. + $remote = implode("/", array_slice(explode("/", $uri), 0, 3)); + + if (!isset($remote)) { + return new JSONResponse("Correct WebDAV URI not found in the request. remote is: $remote", Http::STATUS_BAD_REQUEST); + } + + // TODO: @Mahdi write checks for webapp and transfer protocols missing properties and return STATUS_BAD_REQUEST. + + // remove trailing "/" from share name. + $name = rtrim($name, "/"); + + // prepare data for adding to the native efss table. + $efssShareData = [ + // "https://nc1.docker" + "remote" => $remote, + // the id of the share in the oc_share table of the remote. + "remote_id" => $remoteShareId, + // "tDPRTrLI4hE3C5T" + "share_token" => $sharedSecret, + // password is always null. ScienceMesh doesn't have password protected shares. + "password" => null, + // "TestFolder" + "name" => $name, + // "einstein" + "owner" => $ownerOpaqueId, + // receiver "marie" + "user" => $userId + ]; + + $efssShareId = $this->shareProvider->addReceivedOcmShareToEfssTable($efssShareData); + + // prepare data for adding to the ScienceMesh OCM table. + // see: https://github.com/sciencemesh/nc-sciencemesh/issues/45 + $expiration = $params["expiration"] ?? null; + if (isset($expiration)) { + $expiration = (int)$expiration; + } + + $ocmShareData = [ + "share_external_id" => $efssShareId, + "name" => $name, + "share_with" => $granteeOpaqueId . "@" . $granteeIdp, + "owner" => $ownerOpaqueId . "@" . $ownerIdp, + "initiator" => $creatorOpaqueId . "@" . $creatorIdp, + "ctime" => $ctime, + "mtime" => $mtime, + "expiration" => $expiration, + "remote_share_id" => $remoteShareId, + "transfer" => $ocmProtocolTransfer ?? null, + "webapp" => $ocmProtocolWebapp ?? null, + "webdav" => $ocmProtocolWebdav, ]; - $id = $this->shareProvider->addScienceMeshShare($scienceMeshData, $shareData); - return new JSONResponse($id, 201); + $this->shareProvider->addReceivedOcmShareToSciencemeshTable($ocmShareData); + + return new JSONResponse($efssShareId, Http::STATUS_CREATED); } /** * Remove Share from share table + * * @PublicPage * @NoCSRFRequired * @param $userId @@ -1421,12 +1814,13 @@ public function UpdateSentShare($userId): JSONResponse $share->setPermissions($permissionsCode); $shareUpdated = $this->shareProvider->update($share); - $response = $this->shareInfoToCs3Share($shareUpdated); + $response = $this->shareInfoToCs3Share($shareUpdated, "sent"); return new JSONResponse($response, Http::STATUS_OK); } /** * UpdateReceivedShare updates the received share with share state. + * * @PublicPage * @NoCSRFRequired * @param $userId @@ -1451,7 +1845,7 @@ public function UpdateReceivedShare($userId): JSONResponse $share = $this->shareProvider->getReceivedShareByToken($resourceId); $share->setPermissions($permissionsCode); $shareUpdate = $this->shareProvider->UpdateReceivedShare($share); - $response = $this->shareInfoToCs3Share($shareUpdate, $resourceId); + $response = $this->shareInfoToCs3Share($shareUpdate, "received", $resourceId); $response["state"] = 2; return new JSONResponse($response, Http::STATUS_OK); } catch (Exception $e) { @@ -1462,6 +1856,7 @@ public function UpdateReceivedShare($userId): JSONResponse /** * ListSentShares returns the shares created by the user. If md is provided is not nil, * it returns only shares attached to the given resource. + * * @PublicPage * @NoCSRFRequired * @param $userId @@ -1485,7 +1880,7 @@ public function ListSentShares($userId): JSONResponse if ($shares) { foreach ($shares as $share) { - $responses[] = $this->shareInfoToCs3Share($share); + $responses[] = $this->shareInfoToCs3Share($share, "sent"); } } return new JSONResponse($responses, Http::STATUS_OK); @@ -1493,6 +1888,7 @@ public function ListSentShares($userId): JSONResponse /** * ListReceivedShares returns the list of shares the user has access. + * * @PublicPage * @NoCSRFRequired * @param $userId @@ -1515,7 +1911,7 @@ public function ListReceivedShares($userId): JSONResponse if ($shares) { foreach ($shares as $share) { - $response = $this->shareInfoToCs3Share($share); + $response = $this->shareInfoToCs3Share($share, "received"); $responses[] = [ "share" => $response, "state" => 2 @@ -1528,6 +1924,7 @@ public function ListReceivedShares($userId): JSONResponse /** * GetReceivedShare returns the information for a received share the user has access. + * * @PublicPage * @NoCSRFRequired * @param $userId @@ -1548,7 +1945,7 @@ public function GetReceivedShare($userId): JSONResponse try { $share = $this->shareProvider->getReceivedShareByToken($opaqueId); - $response = $this->shareInfoToCs3Share($share, $opaqueId); + $response = $this->shareInfoToCs3Share($share, "received", $opaqueId); $response["state"] = 2; return new JSONResponse($response, Http::STATUS_OK); } catch (Exception $e) { @@ -1558,6 +1955,7 @@ public function GetReceivedShare($userId): JSONResponse /** * GetSentShare gets the information for a share by the given ref. + * * @PublicPage * @NoCSRFRequired * @param $userId @@ -1581,7 +1979,7 @@ public function GetSentShare($userId): JSONResponse $share = $this->shareProvider->getSentShareByName($userId, $name); if ($share) { - $response = $this->shareInfoToCs3Share($share); + $response = $this->shareInfoToCs3Share($share, "sent"); return new JSONResponse($response, Http::STATUS_OK); } diff --git a/lib/Migration/Version0001Date20211201101630.php b/lib/Migration/Version0001Date20211201101630.php deleted file mode 100644 index 0dcc91e5..00000000 --- a/lib/Migration/Version0001Date20211201101630.php +++ /dev/null @@ -1,100 +0,0 @@ -connection = $connection; - } - - public function changeSchema(IOutput $output, Closure $schemaClosure, array $options) - { - $schema = $schemaClosure(); - $shares = $schema->createTable('sciencemesh_shares'); - $users = $schema->createTable('sciencemesh_users'); - $shares->addColumn( - 'opaque_id', - \OCP\DB\Types::STRING, - ['notnull' => true] - ); - $shares->addColumn( - 'resource_id', - \OCP\DB\Types::STRING, - ['notnull' => true] - ); - $shares->addColumn( - 'permissions', - \OCP\DB\Types::INTEGER, - [] - ); - $shares->addColumn( - 'grantee', - \OCP\DB\Types::INTEGER, - [] - ); - $shares->addColumn( - 'owner', - \OCP\DB\Types::INTEGER, - [] - ); - $shares->addColumn( - 'creator', - \OCP\DB\Types::INTEGER, - [] - ); - $shares->addColumn( - 'ctime', - \OCP\DB\Types::INTEGER, - ['notnull' => true] - ); - $shares->addColumn( - 'mtime', - \OCP\DB\Types::INTEGER, - ['notnull' => true] - ); - $shares->addColumn( - 'is_external', - \OCP\DB\Types::BOOLEAN, - ['notnull' => false] - ); - $shares->addColumn( - 'foreign_id', - \OCP\DB\Types::INTEGER, - [] - ); - $shares->setPrimaryKey(['opaque_id']); - $users->addColumn( - 'id', - \OCP\DB\Types::INTEGER, - ['notnull' => true, 'autoincrement' => true, 'unsigned' => true] - ); - $users->addColumn( - 'idp', - \OCP\DB\Types::STRING, - ['notnull' => true] - ); - $users->addColumn( - 'opaque_id', - \OCP\DB\Types::STRING, - ['notnull' => true] - ); - $users->addColumn( - 'type', - \OCP\DB\Types::INTEGER, - [] - ); - $users->setPrimaryKey(['id']); - return $schema; - } -} diff --git a/lib/Migration/Version010000Date20210118175358.php b/lib/Migration/Version010000Date20210118175358.php deleted file mode 100644 index a2dda8dd..00000000 --- a/lib/Migration/Version010000Date20210118175358.php +++ /dev/null @@ -1,87 +0,0 @@ -hasTable('sciencemesh')) { - $table = $schema->createTable("sciencemesh"); - $table->addColumn('apikey', 'string', [ - 'notnull' => true, - ]); - $table->addColumn('sitename', 'string', [ - 'notnull' => true, - ]); - $table->addColumn('siteurl', 'string', [ - 'notnull' => true, - ]); - $table->addColumn('siteid', 'string', [ - 'notnull' => false, - ]); - $table->addColumn('country', 'string', [ - 'notnull' => true, - 'length' => 3, - ]); - $table->addColumn('iopurl', 'string', [ - 'notnull' => true, - ]); - $table->addColumn('numusers', Types::BIGINT, [ - 'notnull' => true, - 'default' => 0, - 'unsigned' => true, - ]); - $table->addColumn('numfiles', Types::BIGINT, [ - 'notnull' => true, - 'default' => 0, - 'unsigned' => true, - ]); - $table->addColumn('numstorage', Types::BIGINT, [ - 'notnull' => true, - 'default' => 0, - 'unsigned' => true, - ]); - } - return $schema; - } - - /** - * @param IOutput $output - * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` - * @param array $options - */ - public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options) - { - } -} diff --git a/lib/Plugins/ScienceMeshSearchPlugin.php b/lib/Plugins/ScienceMeshSearchPlugin.php index 65f0d1ab..11fa6289 100644 --- a/lib/Plugins/ScienceMeshSearchPlugin.php +++ b/lib/Plugins/ScienceMeshSearchPlugin.php @@ -12,6 +12,7 @@ namespace OCA\ScienceMesh\Plugins; +use Exception; use OC\Share\Constants; use OCA\ScienceMesh\AppInfo\ScienceMeshApp; use OCA\ScienceMesh\RevaHttpClient; @@ -21,6 +22,7 @@ use OCP\Util\UserSearch; use function explode; use function is_array; +use function strtolower; use function substr_count; class ScienceMeshSearchPlugin @@ -44,8 +46,13 @@ class ScienceMeshSearchPlugin /** @var string */ private string $userId = ''; + + /** @var RevaHttpClient */ private RevaHttpClient $revaHttpClient; + /** + * @throws Exception + */ public function __construct( IManager $contactsManager, IConfig $config, @@ -117,11 +124,11 @@ public function search($search): array $cloudIds = [$cloudIds]; } - $lowerSearch = \strtolower($search); + $lowerSearch = strtolower($search); foreach ($cloudIds as $cloudId) { list(, $serverUrl) = $this->splitUserRemote($cloudId); - if (\strtolower($cloudId) === $lowerSearch) { + if (strtolower($cloudId) === $lowerSearch) { $foundRemoteById = true; // Save this as an exact match and continue with next CLOUD $otherResults[] = [ @@ -150,7 +157,7 @@ public function search($search): array } foreach ($values as $value) { // check if we have an exact match - if (\strtolower($value) === $lowerSearch) { + if (strtolower($value) === $lowerSearch) { $this->result['exact']['remotes'][] = [ 'label' => $contact['FN'], 'value' => [ diff --git a/lib/RevaHttpClient.php b/lib/RevaHttpClient.php index 7e319de5..ad00cc54 100644 --- a/lib/RevaHttpClient.php +++ b/lib/RevaHttpClient.php @@ -158,6 +158,7 @@ public function createShare(string $user, array $params) throw new Exception("Missing type", 400); } if (!isset($params['role'])) { + // TODO: @Mahdi why is thi hard coded? is it related to share permissions? $params['role'] = 'viewer'; } if (!isset($params['recipientUsername'])) { diff --git a/lib/ServerConfig.php b/lib/ServerConfig.php index e9a0a597..8a83076d 100644 --- a/lib/ServerConfig.php +++ b/lib/ServerConfig.php @@ -21,18 +21,18 @@ */ function random_str( int $length = 64, - string $keyspace = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' + string $keyspace = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" ): string { if ($length < 1) { throw new RangeException("Length must be a positive integer"); } $pieces = []; - $max = mb_strlen($keyspace, '8bit') - 1; + $max = mb_strlen($keyspace, "8bit") - 1; for ($i = 0; $i < $length; ++$i) { $pieces [] = $keyspace[random_int(0, $max)]; } - return implode('', $pieces); + return implode("", $pieces); } /** @@ -54,32 +54,40 @@ public function __construct(IConfig $config) public function getApiKey() { - return $this->config->getAppValue('sciencemesh', 'apiKey'); + return $this->config->getAppValue("sciencemesh", "apiKey"); } public function getSiteName() { - return $this->config->getAppValue('sciencemesh', 'siteName'); + return $this->config->getAppValue("sciencemesh", "siteName"); } public function getSiteUrl() { - return $this->config->getAppValue('sciencemesh', 'siteUrl'); + return $this->config->getAppValue("sciencemesh", "siteUrl"); } public function getSiteId() { - return $this->config->getAppValue('sciencemesh', 'siteId'); + return $this->config->getAppValue("sciencemesh", "siteId"); } public function getCountry() { - return $this->config->getAppValue('sciencemesh', 'country'); + return $this->config->getAppValue("sciencemesh", "country"); } public function getIopUrl(): string { - return rtrim($this->config->getAppValue('sciencemesh', 'iopUrl'), '/') . '/'; + return rtrim($this->config->getAppValue("sciencemesh", "iopUrl"), "/") . "/"; + } + + public function getIopIdp(): string + { + // TODO: @Mahdi use function from utils. + // converts https://revaowncloud1.docker/ to revaowncloud1.docker + // NOTE: do not use it on anything without http(s) in the start, it would return null. + return str_ireplace("www.", "", parse_url($this->getIopUrl(), PHP_URL_HOST)); } /** @@ -87,10 +95,10 @@ public function getIopUrl(): string */ public function getRevaLoopbackSecret() { - $ret = $this->config->getAppValue('sciencemesh', 'revaLoopbackSecret'); + $ret = $this->config->getAppValue("sciencemesh", "revaLoopbackSecret"); if (!$ret) { $ret = random_str(32); - $this->config->setAppValue('sciencemesh', 'revaLoopbackSecret', $ret); + $this->config->setAppValue("sciencemesh", "revaLoopbackSecret", $ret); } return $ret; } @@ -100,76 +108,76 @@ public function getRevaLoopbackSecret() */ public function getRevaSharedSecret() { - $ret = $this->config->getAppValue('sciencemesh', 'revaSharedSecret'); + $ret = $this->config->getAppValue("sciencemesh", "revaSharedSecret"); if (!$ret) { $ret = random_str(32); - $this->config->setAppValue('sciencemesh', 'revaSharedSecret', $ret); + $this->config->setAppValue("sciencemesh", "revaSharedSecret", $ret); } return $ret; } public function setRevaSharedSecret($sharedSecret) { - $this->config->setAppValue('sciencemesh', 'revaSharedSecret', $sharedSecret); + $this->config->setAppValue("sciencemesh", "revaSharedSecret", $sharedSecret); } public function getNumUsers() { - return $this->config->getAppValue('sciencemesh', 'numUsers'); + return $this->config->getAppValue("sciencemesh", "numUsers"); } public function getNumFiles() { - return $this->config->getAppValue('sciencemesh', 'numFiles'); + return $this->config->getAppValue("sciencemesh", "numFiles"); } public function getNumStorage() { - return $this->config->getAppValue('sciencemesh', 'numStorage'); + return $this->config->getAppValue("sciencemesh", "numStorage"); } public function setApiKey($apiKey) { - $this->config->setAppValue('sciencemesh', 'apiKey', $apiKey); + $this->config->setAppValue("sciencemesh", "apiKey", $apiKey); } public function setSiteName($siteName) { - $this->config->setAppValue('sciencemesh', 'siteName', $siteName); + $this->config->setAppValue("sciencemesh", "siteName", $siteName); } public function setSiteUrl($siteUrl) { - $this->config->setAppValue('sciencemesh', 'siteUrl', $siteUrl); + $this->config->setAppValue("sciencemesh", "siteUrl", $siteUrl); } public function setSiteId($siteId) { - $this->config->setAppValue('sciencemesh', 'siteId', $siteId); + $this->config->setAppValue("sciencemesh", "siteId", $siteId); } public function setCountry($country) { - $this->config->setAppValue('sciencemesh', 'country', $country); + $this->config->setAppValue("sciencemesh", "country", $country); } public function setIopUrl($iopUrl) { - $this->config->setAppValue('sciencemesh', 'iopUrl', $iopUrl); + $this->config->setAppValue("sciencemesh", "iopUrl", $iopUrl); } public function setNumUsers($numUsers) { - $this->config->setAppValue('sciencemesh', 'numUsers', $numUsers); + $this->config->setAppValue("sciencemesh", "numUsers", $numUsers); } public function setNumFiles($numFiles) { - $this->config->setAppValue('sciencemesh', 'numFiles', $numFiles); + $this->config->setAppValue("sciencemesh", "numFiles", $numFiles); } public function setNumStorage($numStorage) { - $this->config->setAppValue('sciencemesh', 'numStorage', $numStorage); + $this->config->setAppValue("sciencemesh", "numStorage", $numStorage); } } diff --git a/lib/ShareProvider/ScienceMeshShareProvider.php b/lib/ShareProvider/ScienceMeshShareProvider.php index 106a4b59..765b41f4 100644 --- a/lib/ShareProvider/ScienceMeshShareProvider.php +++ b/lib/ShareProvider/ScienceMeshShareProvider.php @@ -24,8 +24,9 @@ use OCA\FederatedFileSharing\TokenHandler; use OCA\ScienceMesh\AppInfo\ScienceMeshApp; use OCA\ScienceMesh\RevaHttpClient; -use OCP\Constants; +use OCA\ScienceMesh\ServerConfig; use OCP\Files\IRootFolder; +use OCP\Files\NotFoundException; use OCP\IConfig; use OCP\IDBConnection; use OCP\IL10N; @@ -44,12 +45,15 @@ class ScienceMeshShareProvider extends FederatedShareProviderCopy { - /** @var RevaHttpClient */ - protected RevaHttpClient $revaHttpClient; + /** @var ServerConfig */ + private ServerConfig $serverConfig; /** @var array */ protected array $supportedShareType; + /** @var RevaHttpClient */ + protected RevaHttpClient $revaHttpClient; + /** * ScienceMeshShareProvider constructor. * @@ -91,8 +95,9 @@ public function __construct( $userManager ); - $this->supportedShareType[] = ScienceMeshApp::SHARE_TYPE_SCIENCEMESH; + $this->serverConfig = new ServerConfig($config); $this->revaHttpClient = new RevaHttpClient($config); + $this->supportedShareType[] = ScienceMeshApp::SHARE_TYPE_SCIENCEMESH; } /** @@ -108,119 +113,10 @@ public function isShareTypeSupported(int $shareType): bool } /** - * Share a path - * - * @param IShare $share - * @return IShare The share object - * @throws ShareNotFound - * @throws Exception - */ - public function createInternal(IShare $share): IShare - { - error_log("SMSP: createInternal"); - $shareWith = $share->getSharedWith(); - - error_log("shareWith $shareWith"); - - error_log("checking if already shared " . $share->getNode()->getName()); - /* - * Check if file is not already shared with the remote user - */ - $alreadyShared = $this->getSharedWith($shareWith, $share->getShareType(), $share->getNode(), 1, 0); - if (!empty($alreadyShared)) { - $message = 'Sharing %1$s failed, because this item is already shared with %2$s'; - $message_t = $this->l->t('Sharing %1$s failed, because this item is already shared with user %2$s', [$share->getNode()->getName(), $shareWith]); - $this->logger->debug(sprintf($message, $share->getNode()->getName(), $shareWith), ['app' => 'ScienceMesh']); - throw new Exception($message_t); - } - - - // FIXME: don't allow ScienceMesh shares if source and target server are the same - // ScienceMesh shares always have read permissions - if (($share->getPermissions() & Constants::PERMISSION_READ) === 0) { - $message = 'ScienceMesh shares require read permissions'; - $message_t = $this->l->t('ScienceMesh shares require read permissions'); - $this->logger->debug($message, ['app' => 'ScienceMesh']); - throw new Exception($message_t); - } - - $share->setSharedWith($shareWith); - $shareId = $this->createScienceMeshShare($share); - - $data = $this->getRawShare($shareId); - return $this->createShareObject($data); - } - - /** - * create sciencemesh share and inform the recipient - * - * @param IShare $share - * @return int - * @throws Exception - */ - protected function createScienceMeshShare(IShare $share): int - { - return $this->addSentShareToDB( - $share->getNodeId(), - $share->getNodeType(), - $share->getSharedWith(), - $share->getSharedBy(), - $share->getShareOwner(), - $share->getPermissions(), - $share->getToken(), - $share->getShareType() - ); - } - - /** - * add share to the database and return the ID + * Share a path. * - * @param int $itemSource - * @param string $itemType - * @param string $shareWith - * @param string $sharedBy - * @param string $uidOwner - * @param int $permissions - * @param string $token - * @param int $shareType - * @return int - */ - protected function addSentShareToDB( - int $itemSource, - string $itemType, - string $shareWith, - string $sharedBy, - string $uidOwner, - int $permissions, - string $token, - int $shareType - ): int - { - $qb = $this->dbConnection->getQueryBuilder(); - $qb->insert('share') - ->setValue('share_type', $qb->createNamedParameter($shareType)) - ->setValue('item_type', $qb->createNamedParameter($itemType)) - ->setValue('item_source', $qb->createNamedParameter($itemSource)) - ->setValue('file_source', $qb->createNamedParameter($itemSource)) - ->setValue('share_with', $qb->createNamedParameter($shareWith)) - ->setValue('uid_owner', $qb->createNamedParameter($uidOwner)) - ->setValue('uid_initiator', $qb->createNamedParameter($sharedBy)) - ->setValue('permissions', $qb->createNamedParameter($permissions)) - ->setValue('token', $qb->createNamedParameter($token)) - ->setValue('stime', $qb->createNamedParameter(time())); - - /* - * Added to fix https://github.com/owncloud/core/issues/22215 - * Can be removed once we get rid of ajax/share.php - */ - $qb->setValue('file_target', $qb->createNamedParameter('')); - - $qb->execute(); - return $qb->getLastInsertId(); - } - - /** - * Share a path + * This method is being called by: + * https://github.com/owncloud/core/blob/e95e978f452787e55515ac3e43763e4adcf87d14/lib/private/Share20/Manager.php#L832C3-L832C3 * * @param IShare $share * @return IShare The share object @@ -232,17 +128,21 @@ public function create(IShare $share): IShare $node = $share->getNode(); $shareWith = $share->getSharedWith(); - // This is the routing flag for sending a share. + // this is the routing flag for sending a share. // if the recipient of the share is a sciencemesh contact, // the search plugin will mark it by a postfix. $isSciencemeshUser = $this->stringEndsWith($shareWith, ScienceMeshApp::SCIENCEMESH_POSTFIX); - // Based on the flag, the share will be sent through sciencemesh or regular share provider. + // based on the flag, the share will be sent through sciencemesh or regular share provider. if ($isSciencemeshUser) { // remove the postfix flag from the string. $shareWith = str_replace(ScienceMeshApp::SCIENCEMESH_POSTFIX, "", $shareWith); + error_log("create: node path: " . $node->getPath()); + // node path: /einstein/files/test + // path parts: ["einstein", "files", "test"] $pathParts = explode("/", $node->getPath()); + // sender: einstein $sender = $pathParts[1]; $sourceOffset = 3; $targetOffset = 3; @@ -250,10 +150,13 @@ public function create(IShare $share): IShare $suffix = ($node->getType() == "dir" ? "/" : ""); // "home" is reva's default work space name, prepending that in the source path: + // source path: /test/ + // target path: /home/test/ $sourcePath = $prefix . "home/" . implode("/", array_slice($pathParts, $sourceOffset)) . $suffix; $targetPath = $prefix . implode("/", array_slice($pathParts, $targetOffset)) . $suffix; - // TODO: make a function for below operation. it is used in a lot placed, but incorrectly. + // TODO: @Mahdi make a function for below operation. it is used in a lot placed, but incorrectly. + // TODO: @Mahdi Move to utils. // it should split username@host into an array of 2 element // representing array[0] = username, array[1] = host // requirement: @@ -261,21 +164,72 @@ public function create(IShare $share): IShare // example: MahdiBaghbani@pondersource@sciencemesh.org // username: MahdiBaghbani@pondersource // host: sciencemesh.org - $split_point = '@'; + $split_point = "@"; $parts = explode($split_point, $shareWith); $last = array_pop($parts); $shareWithParts = array(implode($split_point, $parts), $last); + // don't allow ScienceMesh shares if source and target server are the same. + // this means users with the same reva iop cannot share with each other via sciencemesh and + // should use their native efss capabilities to do so. + // see: https://github.com/sciencemesh/nc-sciencemesh/issues/57 + if ($this->serverConfig->getIopIdp() === $shareWithParts[1]) { + $message = "Not allowed to create a ScienceMesh share for a user on the same server %s as sender."; + $message_t = $this->l->t( + "Not allowed to create a ScienceMesh share for a user on the same server %s as sender.", + [$shareWithParts[1]] + ); + $this->logger->debug( + sprintf( + $message, $shareWithParts[1] + ), + ["app" => "sciencemesh"] + ); + throw new Exception($message_t); + } + + // check if file is not already shared with the remote user. + $alreadyShared = $this->getSharedWith( + $shareWith, + $share->getShareType(), + $share->getNode(), + 1, + 0 + ); + + if (!empty($alreadyShared)) { + $message = "Sharing %s failed, because this item is already shared with %s"; + $message_t = $this->l->t( + "Sharing %s failed, because this item is already shared with %s", + [$share->getNode()->getName(), $share->getSharedWith()] + ); + $this->logger->debug( + sprintf( + $message, $share->getNode()->getName(), $share->getSharedWith() + ), + ["app" => "sciencemesh"] + ); + throw new Exception($message_t); + } + $response = $this->revaHttpClient->createShare($sender, [ - 'sourcePath' => $sourcePath, - 'targetPath' => $targetPath, - 'type' => $node->getType(), - 'recipientUsername' => $shareWithParts[0], - 'recipientHost' => $shareWithParts[1] + "sourcePath" => $sourcePath, + "targetPath" => $targetPath, + "type" => $node->getType(), + "recipientUsername" => $shareWithParts[0], + "recipientHost" => $shareWithParts[1] ]); - if (!isset($response) || !isset($response->share) || !isset($response->share->owner) || !isset($response->share->owner->idp)) { - throw new Exception("Unexpected response from reva"); + if (!isset($response->share->owner->idp)) { + $message = "Unexpected response from Reva, response share owner idp doesn't exist."; + $message_t = $this->l->t( + "Unexpected response from Reva, response share owner idp doesn't exist.", + ); + $this->logger->error( + $message, + ["app" => "sciencemesh"] + ); + throw new Exception($message_t); } $share->setId("will-set-this-later"); @@ -288,6 +242,27 @@ public function create(IShare $share): IShare return $share; } + /** + * Share a path via ScienceMesh. + * + * @param IShare $share + * @return IShare The share object + * @throws NotFoundException + * @throws ShareNotFound + * @throws InvalidShare + * @throws IllegalIDChangeException + */ + public function createNativeEfssScienceMeshShare(IShare $share): IShare + { + // this adds share to native efss table in the database. + $shareId = $this->addSentOcmShareToEfssTable($share); + + $data = $this->getRawShare($shareId); + return $this->createShareObject($data); + } + + // TODO: @Mahdi Move to utils. + /** * Check if a given string ends with a substring. * @@ -316,9 +291,9 @@ public function updateReceivedShare(IShare $share): IShare * We allow updating the permissions of sciencemesh shares */ $qb = $this->dbConnection->getQueryBuilder(); - $qb->update('share_external') - ->where($qb->expr()->eq('id', $qb->createNamedParameter($share->getId()))) - ->set('owner', $qb->createNamedParameter($share->getShareOwner())) + $qb->update("share_external") + ->where($qb->expr()->eq("id", $qb->createNamedParameter($share->getId()))) + ->set("owner", $qb->createNamedParameter($share->getShareOwner())) ->execute(); return $share; } @@ -333,14 +308,16 @@ public function updateReceivedShare(IShare $share): IShare public function getReceivedShareByToken(string $token) { $qb = $this->dbConnection->getQueryBuilder(); - $cursor = $qb->select('*') - ->from('share_external') - ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(14))) - ->andWhere($qb->expr()->eq('share_token', $qb->createNamedParameter($token))) + $cursor = $qb->select("*") + ->from("share_external") + ->where($qb->expr()->eq("share_type", $qb->createNamedParameter(14))) + ->andWhere($qb->expr()->eq("share_token", $qb->createNamedParameter($token))) ->execute(); $data = $cursor->fetch(); + $cursor->closeCursor(); + if ($data === false) { - throw new ShareNotFound('Share not found', $this->l->t('Could not find share')); + throw new ShareNotFound("Share not found", $this->l->t("Could not find share")); } return $this->createExternalShareObject($data); } @@ -355,12 +332,12 @@ public function getReceivedShareByToken(string $token) protected function createExternalShareObject(array $data) { $share = new Share($this->rootFolder, $this->userManager); - $share->setId((int)$data['id']) - ->setShareType((int)$data['share_type']) - ->setShareOwner($data['owner']) - ->setSharedBy($data['owner']) - ->setToken($data['share_token']) - ->setSharedWith($data['user']); + $share->setId((int)$data["id"]) + ->setShareType((int)$data["share_type"]) + ->setShareOwner($data["owner"]) + ->setSharedBy($data["owner"]) + ->setToken($data["share_token"]) + ->setSharedWith($data["user"]); $share->setProviderId($this->identifier()); return $share; @@ -373,7 +350,7 @@ protected function createExternalShareObject(array $data) */ public function identifier(): string { - return 'sciencemesh'; + return "sciencemesh"; } /** @@ -386,22 +363,27 @@ public function identifier(): string public function getSentShareByToken(string $token): IShare { error_log("share provider getSentShareByToken '$token'"); + $qb = $this->dbConnection->getQueryBuilder(); - $cursor = $qb->select('*') - ->from('share') - ->where($qb->expr()->eq('token', $qb->createNamedParameter($token))) + $cursor = $qb->select("*") + ->from("share") + ->where($qb->expr()->eq("token", $qb->createNamedParameter($token))) ->execute(); $data = $cursor->fetch(); + $cursor->closeCursor(); + if ($data === false) { error_log("sent share not found by token '$token'"); - throw new ShareNotFound('Share not found', $this->l->t('Could not find share')); + throw new ShareNotFound("Share not found", $this->l->t("Could not find share")); } + try { $share = $this->createShareObject($data); } catch (InvalidShare $e) { error_log("sent share found invalid by token '$token'"); - throw new ShareNotFound('Share not found', $this->l->t('Could not find share')); + throw new ShareNotFound("Share not found", $this->l->t("Could not find share")); } + error_log("found sent share " . $data["id"] . " by token '$token'"); return $share; } @@ -410,15 +392,15 @@ public function getSentShares(int $userId): iterable { $qb = $this->dbConnection->getQueryBuilder(); - $qb->select('*') - ->from('share') + $qb->select("*") + ->from("share") ->where( - $qb->expr()->eq('share_type', $qb->createNamedParameter(ScienceMeshApp::SHARE_TYPE_SCIENCEMESH)) + $qb->expr()->eq("share_type", $qb->createNamedParameter(ScienceMeshApp::SHARE_TYPE_SCIENCEMESH)) ) ->andWhere( $qb->expr()->orX( - $qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId)), - $qb->expr()->eq('uid_owner', $qb->createNamedParameter($userId)) + $qb->expr()->eq("uid_initiator", $qb->createNamedParameter($userId)), + $qb->expr()->eq("uid_owner", $qb->createNamedParameter($userId)) ) ); @@ -438,10 +420,10 @@ public function getSentShares(int $userId): iterable public function getReceivedShares($userId): iterable { $qb = $this->dbConnection->getQueryBuilder(); - $qb->select('*') - ->from('share_external') + $qb->select("*") + ->from("share_external") ->where( - $qb->expr()->eq('user', $qb->createNamedParameter($userId)) + $qb->expr()->eq("user", $qb->createNamedParameter($userId)) ); $cursor = $qb->execute(); while ($data = $cursor->fetch()) { @@ -459,34 +441,38 @@ public function getReceivedShares($userId): iterable public function deleteSentShareByName($userId, $name): bool { $qb = $this->dbConnection->getQueryBuilder(); - $qb->select('fileid') - ->from('filecache') + $qb->select("fileid") + ->from("filecache") ->where( - $qb->expr()->eq('name', $qb->createNamedParameter($name)) + $qb->expr()->eq("name", $qb->createNamedParameter($name)) ); $cursor = $qb->execute(); $data = $cursor->fetch(); + $cursor->closeCursor(); + if (!$data) { return false; } - $id = $data['fileid']; - $isShare = $qb->select('*') - ->from('share') + + + $id = $data["fileid"]; + $isShare = $qb->select("*") + ->from("share") ->where( - $qb->expr()->eq('uid_owner', $qb->createNamedParameter($userId)) + $qb->expr()->eq("uid_owner", $qb->createNamedParameter($userId)) ) ->andWhere( - $qb->expr()->eq('item_source', $qb->createNamedParameter($id)) + $qb->expr()->eq("item_source", $qb->createNamedParameter($id)) ) ->execute() ->fetch(); if ($isShare) { - $qb->delete('share') + $qb->delete("share") ->where( - $qb->expr()->eq('uid_owner', $qb->createNamedParameter($userId)) + $qb->expr()->eq("uid_owner", $qb->createNamedParameter($userId)) ) ->andWhere( - $qb->expr()->eq('item_source', $qb->createNamedParameter($id)) + $qb->expr()->eq("item_source", $qb->createNamedParameter($id)) ); $qb->execute(); return true; @@ -509,25 +495,27 @@ public function delete(IShare $share) public function deleteReceivedShareByOpaqueId($userId, $opaqueId): bool { $qb = $this->dbConnection->getQueryBuilder(); - $qb->select('*') - ->from('share_external') + $qb->select("*") + ->from("share_external") ->where( - $qb->expr()->eq('user', $qb->createNamedParameter($userId)) + $qb->expr()->eq("user", $qb->createNamedParameter($userId)) ) ->andWhere( - $qb->expr()->eq('share_token', $qb->createNamedParameter($opaqueId)) + $qb->expr()->eq("share_token", $qb->createNamedParameter($opaqueId)) ); $cursor = $qb->execute(); $data = $cursor->fetch(); + $cursor->closeCursor(); + if (!$data) { return false; } else { - $qb->delete('share_external') + $qb->delete("share_external") ->where( - $qb->expr()->eq('user', $qb->createNamedParameter($userId)) + $qb->expr()->eq("user", $qb->createNamedParameter($userId)) ) ->andWhere( - $qb->expr()->eq('share_token', $qb->createNamedParameter($opaqueId)) + $qb->expr()->eq("share_token", $qb->createNamedParameter($opaqueId)) ); $qb->execute(); return true; @@ -542,36 +530,43 @@ public function getSentShareByPath($userId, $path) { $qb = $this->dbConnection->getQueryBuilder(); - $qb->select('fileid') - ->from('filecache') + $qb->select("fileid") + ->from("filecache") ->where( - $qb->expr()->eq('path', $qb->createNamedParameter($path)) + $qb->expr()->eq("path", $qb->createNamedParameter($path)) ); + $cursor = $qb->execute(); $data = $cursor->fetch(); + $cursor->closeCursor(); + if (!$data) { return false; } - $id = $data['fileid']; - $qb->select('*') - ->from('share') + $id = $data["fileid"]; + $qb->select("*") + ->from("share") ->where( - $qb->expr()->eq('uid_owner', $qb->createNamedParameter($userId)) + $qb->expr()->eq("uid_owner", $qb->createNamedParameter($userId)) ) ->andWhere( - $qb->expr()->eq('item_source', $qb->createNamedParameter($id)) + $qb->expr()->eq("item_source", $qb->createNamedParameter($id)) ); + $cursor = $qb->execute(); $data = $cursor->fetch(); + $cursor->closeCursor(); + if (!$data) { return false; } + try { $share = $this->createShareObject($data); } catch (InvalidShare $e) { throw new ShareNotFound(); } - $cursor->closeCursor(); + return $share; } @@ -581,30 +576,35 @@ public function getSentShareByPath($userId, $path) public function getShareByOpaqueId($opaqueId) { $qb = $this->dbConnection->getQueryBuilder(); - $c = $qb->select('is_external') - ->from('sciencemesh_shares') + $cursor = $qb->select("is_external") + ->from("sciencemesh_shares") ->where( - $qb->expr()->eq('opaque_id', $qb->createNamedParameter($opaqueId)) + $qb->expr()->eq("opaque_id", $qb->createNamedParameter($opaqueId)) ) ->execute(); - $data = $c->fetch(); + $data = $cursor->fetch(); + $cursor->closeCursor(); + if (!$data) { return false; } - $external = $data['is_external']; - $c = $qb->select('*') - ->from('sciencemesh_shares', 'sms') - ->innerJoin('sms', $external ? 'share_external' : 'share', 's', $qb->expr()->eq('sms.foreignId', 's.id')) + + $external = $data["is_external"]; + $cursor = $qb->select("*") + ->from("sciencemesh_shares", "sms") + ->innerJoin("sms", $external ? "share_external" : "share", "s", $qb->expr()->eq("sms.foreignId", "s.id")) ->where( - $qb->expr()->eq('sms.opaque_id', $qb->createNamedParameter($opaqueId)) + $qb->expr()->eq("sms.opaque_id", $qb->createNamedParameter($opaqueId)) ) ->execute(); - $data = $c->fetch(); + $data = $cursor->fetch(); + $cursor->closeCursor(); + if (!$data) { return false; } - return $external ? $this->createScienceMeshExternalShare($data) : $this->createScienceMeshShare($data); + return $external ? $this->createScienceMeshExternalShare($data) : $this->addSentOcmShareToEfssTable($data); } public function addScienceMeshUser($user) @@ -613,25 +613,27 @@ public function addScienceMeshUser($user) $opaqueId = $user->getOpaqueId(); $type = $user->getType(); $qb = $this->dbConnection->getQueryBuilder(); - $cursor = $qb->select('*') - ->from('sciencemesh_users') + $cursor = $qb->select("*") + ->from("sciencemesh_users") ->where( - $qb->expr()->eq('idp', $qb->createNamedParameter($idp)) + $qb->expr()->eq("idp", $qb->createNamedParameter($idp)) ) ->andWhere( - $qb->expr()->eq('opaque_id', $qb->createNamedParameter($opaqueId)) + $qb->expr()->eq("opaque_id", $qb->createNamedParameter($opaqueId)) ) ->execute(); $data = $cursor->fetch(); + $cursor->closeCursor(); + if (!$data) { - $qb->insert('sciencemesh_users') - ->setValue('idp', $qb->createNamedParameter($idp)) - ->setValue('opaque_id', $qb->createNamedParameter($opaqueId)) - ->setValue('type', $qb->createNamedParameter($type)) + $qb->insert("sciencemesh_users") + ->setValue("idp", $qb->createNamedParameter($idp)) + ->setValue("opaque_id", $qb->createNamedParameter($opaqueId)) + ->setValue("type", $qb->createNamedParameter($type)) ->execute(); return $qb->getLastInsertId(); } else { - return $data['id']; + return $data["id"]; } } @@ -640,55 +642,384 @@ public function addScienceMeshUser($user) */ public function addScienceMeshShare($scienceMeshData, $shareData): int { - if ($scienceMeshData['is_external']) { - return $this->addReceivedShareToDB($shareData); + if ($scienceMeshData["is_external"]) { + return $this->addReceivedOcmShareToEfssTable($shareData); } else { - return $this->createScienceMeshShare($shareData); + return $this->addSentOcmShareToEfssTable($shareData); } } /** - * add share to the database and return the ID + * add sent ScienceMesh share to the native efss table and return the ID. + * + * @param IShare $share + * @return int + * @throws NotFoundException + */ + protected function addSentOcmShareToEfssTable(IShare $share): int + { + $shareType = $share->getShareType(); + $itemType = $share->getNodeType(); + $itemSource = $share->getNodeId(); + $shareWith = $share->getSharedWith(); + $owner = $share->getShareOwner(); + $sharedBy = $share->getSharedBy(); + $permissions = $share->getPermissions(); + $token = $share->getToken(); + $shareTime = $share->getShareTime()->getTimestamp(); + + $qb = $this->dbConnection->getQueryBuilder(); + $qb->insert("share") + ->setValue("share_type", $qb->createNamedParameter($shareType)) + ->setValue("item_type", $qb->createNamedParameter($itemType)) + ->setValue("item_source", $qb->createNamedParameter($itemSource)) + ->setValue("file_source", $qb->createNamedParameter($itemSource)) + ->setValue("share_with", $qb->createNamedParameter($shareWith)) + ->setValue("uid_owner", $qb->createNamedParameter($owner)) + ->setValue("uid_initiator", $qb->createNamedParameter($sharedBy)) + ->setValue("permissions", $qb->createNamedParameter($permissions)) + ->setValue("token", $qb->createNamedParameter($token)) + ->setValue("stime", $qb->createNamedParameter($shareTime)); + + /* + * Added to fix https://github.com/owncloud/core/issues/22215 + * Can be removed once we get rid of ajax/share.php + */ + $qb->setValue("file_target", $qb->createNamedParameter("")); + + $qb->execute(); + return $qb->getLastInsertId(); + } + + + /** + * add sent ScienceMesh share to the ScienceMesh table and return the ID + * + * @param $shareData + * @return int + */ + public function addSentOcmShareToSciencemeshTable($shareData): int + { + // check if the share already exists in the database. + $qbt = $this->dbConnection->getQueryBuilder(); + $qbt->select("*") + ->from("sciencemesh_ocm_sent_shares") + ->where($qbt->expr()->eq("share_internal_id", $qbt->createNamedParameter($shareData["share_internal_id"]))); + $cursor = $qbt->execute(); + $data = $cursor->fetch(); + $cursor->closeCursor(); + + // return id if share already exists. + if ($data) { + return $data["id"]; + } + + // add ocm share to sciencemesh_ocm_sent_shares table. + $qb = $this->dbConnection->getQueryBuilder(); + $qb->insert("sciencemesh_ocm_sent_shares") + ->setValue("share_internal_id", $qb->createNamedParameter($shareData["share_internal_id"])) + ->setValue("name", $qb->createNamedParameter($shareData["name"])) + ->setValue("share_with", $qb->createNamedParameter($shareData["share_with"])) + ->setValue("owner", $qb->createNamedParameter($shareData["owner"])) + ->setValue("initiator", $qb->createNamedParameter($shareData["initiator"])) + ->setValue("ctime", $qb->createNamedParameter($shareData["ctime"])) + ->setValue("ctime", $qb->createNamedParameter($shareData["ctime"])) + ->setValue("mtime", $qb->createNamedParameter($shareData["mtime"])) + ->setValue("expiration", $qb->createNamedParameter($shareData["expiration"])); + $qb->execute(); + + $id = $qb->getLastInsertId(); + + // add protocols to their tables. + $transfer = $shareData["transfer"] ?? null; + + if (isset($transfer["sourceUri"]) && $transfer["sharedSecret"] && $transfer["size"]) { + $qb = $this->dbConnection->getQueryBuilder(); + $qb->insert("sciencemesh_ocm_sent_share_protocol_transfer") + ->setValue("ocm_sent_share_id", $qb->createNamedParameter($id)) + ->setValue("source_uri", $qb->createNamedParameter($transfer["sourceUri"])) + ->setValue("shared_secret", $qb->createNamedParameter($transfer["sharedSecret"])) + ->setValue("size", $qb->createNamedParameter($transfer["size"])); + $qb->execute(); + } + + $webapp = $shareData["webapp"] ?? null; + + if (isset($webapp["uriTemplate"]) && $webapp["viewMode"]) { + $qb = $this->dbConnection->getQueryBuilder(); + $qb->insert("sciencemesh_ocm_sent_share_protocol_webapp") + ->setValue("ocm_sent_share_id", $qb->createNamedParameter($id)) + ->setValue("uri_template", $qb->createNamedParameter($webapp["uriTemplate"])) + ->setValue("view_mode", $qb->createNamedParameter($webapp["viewMode"])); + $qb->execute(); + } + + $webdav = $shareData["webdav"] ?? null; + + if (isset($webdav["uri"]) && $webdav["sharedSecret"] && $webdav["permissions"]) { + $qb = $this->dbConnection->getQueryBuilder(); + $qb->insert("sciencemesh_ocm_sent_share_protocol_webdav") + ->setValue("ocm_sent_share_id", $qb->createNamedParameter($id)) + ->setValue("uri", $qb->createNamedParameter($webdav["uri"])) + ->setValue("shared_secret", $qb->createNamedParameter($webdav["sharedSecret"])) + ->setValue("permissions", $qb->createNamedParameter($webdav["permissions"])); + $qb->execute(); + } + + return $id; + } + + /** + * add received ScienceMesh share to the native efss table and return the ID. * * @param $shareData * @return int */ - public function addReceivedShareToDB($shareData): int + public function addReceivedOcmShareToEfssTable($shareData): int { - $mountpoint = "{{TemporaryMountPointName#" . $shareData["name"] . "}}"; - $mountpoint_hash = md5($mountpoint); + // calculate the mount point has of the share. + $mountPoint = "{{TemporaryMountPointName#" . $shareData["name"] . "}}"; + $mountPointHash = md5($mountPoint); + + // check if the share already exists in the database. $qbt = $this->dbConnection->getQueryBuilder(); - $qbt->select('*') - ->from('share_external') - ->where($qbt->expr()->eq('user', $qbt->createNamedParameter($shareData["user"]))) - ->andWhere($qbt->expr()->eq('mountpoint_hash', $qbt->createNamedParameter($mountpoint_hash))); + $qbt->select("*") + ->from("share_external") + ->where($qbt->expr()->eq("user", $qbt->createNamedParameter($shareData["user"]))) + ->andWhere($qbt->expr()->eq("mountpoint_hash", $qbt->createNamedParameter($mountPointHash))); $cursor = $qbt->execute(); - if ($data = $cursor->fetch()) { - return $data['id']; + $data = $cursor->fetch(); + $cursor->closeCursor(); + + // return id if share already exists. + if ($data) { + return $data["id"]; } - if (!str_starts_with(strtolower($shareData["remote"]), 'http://') && !str_starts_with(strtolower($shareData["remote"]), 'https://')) { + // NOTE: @Mahdi I don't like this approach. + // prefix remote with https if it doesn't start with http or https. + if (!str_starts_with(strtolower($shareData["remote"]), "http://") && !str_starts_with(strtolower($shareData["remote"]), "https://")) { $shareData["remote"] = "https://" . $shareData["remote"]; } + // TODO: @Mahdi maybe use enums? for better readability. // 0 => pending, 1 => accepted, 2 => rejected - $accepted = 0; //pending + $accepted = 0; $qb = $this->dbConnection->getQueryBuilder(); - $qb->insert('share_external') - ->setValue('remote', $qb->createNamedParameter($shareData["remote"])) - ->setValue('remote_id', $qb->createNamedParameter(trim($shareData["remote_id"], '"'))) - ->setValue('share_token', $qb->createNamedParameter($shareData["share_token"])) - ->setValue('password', $qb->createNamedParameter($shareData["password"])) - ->setValue('name', $qb->createNamedParameter($shareData["name"])) - ->setValue('owner', $qb->createNamedParameter($shareData["owner"])) - ->setValue('user', $qb->createNamedParameter($shareData["user"])) - ->setValue('mountpoint', $qb->createNamedParameter($mountpoint)) - ->setValue('mountpoint_hash', $qb->createNamedParameter($mountpoint_hash)) - ->setValue('accepted', $qb->createNamedParameter($accepted)); + $qb->insert("share_external") + ->setValue("remote", $qb->createNamedParameter($shareData["remote"])) + ->setValue("remote_id", $qb->createNamedParameter(trim($shareData["remote_id"], '"'))) + ->setValue("share_token", $qb->createNamedParameter($shareData["share_token"])) + ->setValue("password", $qb->createNamedParameter($shareData["password"])) + ->setValue("name", $qb->createNamedParameter($shareData["name"])) + ->setValue("owner", $qb->createNamedParameter($shareData["owner"])) + ->setValue("user", $qb->createNamedParameter($shareData["user"])) + ->setValue("mountpoint", $qb->createNamedParameter($mountPoint)) + ->setValue("mountpoint_hash", $qb->createNamedParameter($mountPointHash)) + ->setValue("accepted", $qb->createNamedParameter($accepted)); $qb->execute(); return $qb->getLastInsertId(); } + /** + * add received ScienceMesh share to the ScienceMesh table and return the ID + * + * @param $shareData + * @return int + */ + public function addReceivedOcmShareToSciencemeshTable($shareData): int + { + // check if the share already exists in the database. + $qbt = $this->dbConnection->getQueryBuilder(); + $qbt->select("*") + ->from("sciencemesh_ocm_received_shares") + ->where($qbt->expr()->eq("share_external_id", $qbt->createNamedParameter($shareData["share_external_id"]))); + $cursor = $qbt->execute(); + $data = $cursor->fetch(); + $cursor->closeCursor(); + + // return id if share already exists. + if ($data) { + return $data["id"]; + } + + // add ocm share to sciencemesh_ocm_received_shares table. + $qb = $this->dbConnection->getQueryBuilder(); + $qb->insert("sciencemesh_ocm_received_shares") + ->setValue("share_external_id", $qb->createNamedParameter($shareData["share_external_id"])) + ->setValue("name", $qb->createNamedParameter($shareData["name"])) + ->setValue("share_with", $qb->createNamedParameter($shareData["share_with"])) + ->setValue("owner", $qb->createNamedParameter($shareData["owner"])) + ->setValue("initiator", $qb->createNamedParameter($shareData["initiator"])) + ->setValue("ctime", $qb->createNamedParameter($shareData["ctime"])) + ->setValue("ctime", $qb->createNamedParameter($shareData["ctime"])) + ->setValue("mtime", $qb->createNamedParameter($shareData["mtime"])) + ->setValue("expiration", $qb->createNamedParameter($shareData["expiration"])) + ->setValue("remote_share_id", $qb->createNamedParameter($shareData["remote_share_id"])); + $qb->execute(); + + $id = $qb->getLastInsertId(); + + // add protocols to their tables. + $transfer = $shareData["transfer"] ?? null; + + if (isset($transfer["sourceUri"]) && $transfer["sharedSecret"] && $transfer["size"]) { + $qb = $this->dbConnection->getQueryBuilder(); + $qb->insert("sciencemesh_ocm_received_share_protocol_transfer") + ->setValue("ocm_received_share_id", $qb->createNamedParameter($id)) + ->setValue("source_uri", $qb->createNamedParameter($transfer["sourceUri"])) + ->setValue("shared_secret", $qb->createNamedParameter($transfer["sharedSecret"])) + ->setValue("size", $qb->createNamedParameter($transfer["size"])); + $qb->execute(); + } + + $webapp = $shareData["webapp"] ?? null; + + if (isset($webapp["uriTemplate"]) && isset($webapp["viewMode"])) { + $qb = $this->dbConnection->getQueryBuilder(); + $qb->insert("sciencemesh_ocm_received_share_protocol_webapp") + ->setValue("ocm_received_share_id", $qb->createNamedParameter($id)) + ->setValue("uri_template", $qb->createNamedParameter($webapp["uriTemplate"])) + ->setValue("view_mode", $qb->createNamedParameter($webapp["viewMode"])); + $qb->execute(); + } + + $webdav = $shareData["webdav"] ?? null; + + if (isset($webdav["uri"]) && $webdav["sharedSecret"] && $webdav["permissions"]) { + $qb = $this->dbConnection->getQueryBuilder(); + $qb->insert("sciencemesh_ocm_received_share_protocol_webdav") + ->setValue("ocm_received_share_id", $qb->createNamedParameter($id)) + ->setValue("uri", $qb->createNamedParameter($webdav["uri"])) + ->setValue("shared_secret", $qb->createNamedParameter($webdav["sharedSecret"])) + ->setValue("permissions", $qb->createNamedParameter($webdav["permissions"])); + $qb->execute(); + } + + return $id; + } + + /** + * get all the data about the sent OCM share from ScienceMesh table. + * + * @param $shareId + * @return array|null + */ + public function getSentOcmShareFromSciencemeshTable($shareId): ?array + { + $qbt = $this->dbConnection->getQueryBuilder(); + $qbt->select("*") + ->from("sciencemesh_ocm_sent_shares") + ->where($qbt->expr()->eq("share_internal_id", $qbt->createNamedParameter($shareId))); + $cursor = $qbt->execute(); + $data = $cursor->fetch(); + $cursor->closeCursor(); + + return $data; + } + + /** + * get all the protocols of the sent OCM share from ScienceMesh table. + * + * @param $ocmShareId + * @return array|null + */ + public function getSentOcmShareProtocolsFromSciencemeshTable($ocmShareId): ?array + { + $qbt = $this->dbConnection->getQueryBuilder(); + $qbt->select("*") + ->from("sciencemesh_ocm_sent_share_protocol_transfer") + ->where($qbt->expr()->eq("ocm_sent_share_id", $qbt->createNamedParameter($ocmShareId))); + $cursor = $qbt->execute(); + + $protocolTransfer = $cursor->fetch() ?? null; + $cursor->closeCursor(); + + $qbt = $this->dbConnection->getQueryBuilder(); + $qbt->select("*") + ->from("sciencemesh_ocm_sent_share_protocol_webapp") + ->where($qbt->expr()->eq("ocm_sent_share_id", $qbt->createNamedParameter($ocmShareId))); + $cursor = $qbt->execute(); + + $protocolWebApp = $cursor->fetch() ?? null; + $cursor->closeCursor(); + + $qbt = $this->dbConnection->getQueryBuilder(); + $qbt->select("*") + ->from("sciencemesh_ocm_sent_share_protocol_webdav") + ->where($qbt->expr()->eq("ocm_sent_share_id", $qbt->createNamedParameter($ocmShareId))); + $cursor = $qbt->execute(); + + $protocolWebDav = $cursor->fetch() ?? null; + $cursor->closeCursor(); + + return [ + "transfer" => $protocolTransfer, + "webapp" => $protocolWebApp, + "webdav" => $protocolWebDav, + ]; + } + + /** + * get all the data about the received OCM share from ScienceMesh table. + * + * @param $shareId + * @return array|null + */ + public function getReceivedOcmShareFromSciencemeshTable($shareId): ?array + { + $qbt = $this->dbConnection->getQueryBuilder(); + $qbt->select("*") + ->from("sciencemesh_ocm_received_shares") + ->where($qbt->expr()->eq("share_external_id", $qbt->createNamedParameter($shareId))); + $cursor = $qbt->execute(); + $data = $cursor->fetch(); + $cursor->closeCursor(); + + return $data; + } + + /** + * get all the protocols of the received OCM share from ScienceMesh table. + * + * @param $ocmShareId + * @return array|null + */ + public function getReceivedOcmShareProtocolsFromSciencemeshTable($ocmShareId): ?array + { + $qbt = $this->dbConnection->getQueryBuilder(); + $qbt->select("*") + ->from("sciencemesh_ocm_received_share_protocol_transfer") + ->where($qbt->expr()->eq("ocm_received_share_id", $qbt->createNamedParameter($ocmShareId))); + $cursor = $qbt->execute(); + + $protocolTransfer = $cursor->fetch() ?? null; + $cursor->closeCursor(); + + $qbt = $this->dbConnection->getQueryBuilder(); + $qbt->select("*") + ->from("sciencemesh_ocm_received_share_protocol_webapp") + ->where($qbt->expr()->eq("ocm_received_share_id", $qbt->createNamedParameter($ocmShareId))); + $cursor = $qbt->execute(); + + $protocolWebApp = $cursor->fetch() ?? null; + $cursor->closeCursor(); + + $qbt = $this->dbConnection->getQueryBuilder(); + $qbt->select("*") + ->from("sciencemesh_ocm_received_share_protocol_webdav") + ->where($qbt->expr()->eq("ocm_received_share_id", $qbt->createNamedParameter($ocmShareId))); + $cursor = $qbt->execute(); + + $protocolWebDav = $cursor->fetch() ?? null; + $cursor->closeCursor(); + + return [ + "transfer" => $protocolTransfer, + "webapp" => $protocolWebApp, + "webdav" => $protocolWebDav, + ]; + } protected function revokeShare(IShare $share, bool $isOwner) {