diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..65f6ed7 --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,36 @@ +pipeline { + agent { + node { + label 'nodejs-agent-v1' + } + } + options { + buildDiscarder(logRotator(numToKeepStr: '50')) + } + stages { + stage('Node version') { + steps { + sh '. /usr/bin/load_nvm && nvm use 6' + } + } + stage('Make') { + steps { + sh '. /usr/bin/load_nvm && make clean all' + archiveArtifacts artifacts: "dist/zimbra_drive.tgz", fingerprint: true + archiveArtifacts artifacts: "dist/zimbradrive.tar.gz", fingerprint: true + archiveArtifacts artifacts: "dist/zimbra_drive.md5", fingerprint: true + } + } + } + post { + always { + script { + GIT_COMMIT_EMAIL = sh ( + script: 'git --no-pager show -s --format=\'%ae\'', + returnStdout: true + ).trim() + } + emailext attachLog: true, body: '$DEFAULT_CONTENT', recipientProviders: [requestor()], subject: '$DEFAULT_SUBJECT', to: "${GIT_COMMIT_EMAIL}" + } + } +} diff --git a/README.md b/README.md index 2b8b19b..c1bbd48 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,13 @@ -Zimbra Drive +Open Drive ============ +Open Drive is not not a fully fledged Zextras product under active support and development, but a contribution provided to the Zimbra Community "as is". + +Anyone is free to download it and to clone the repository to apply any change complying with the project's licensing but there is no official commitment on updates, on the inclusion of features and/or the approval of pull requests. + +--- + + Zimbra and Nextcloud / ownCloud integration. Features: @@ -10,7 +17,7 @@ Features: - Attach Nextcloud / ownCloud files to email. Supported Versions: -- Nextcloud: 9, 10, 11, 12 +- Nextcloud: 9, 10, 11, 12, 13 - ownCloud: 9, 9.1, 10 ## Install @@ -123,3 +130,6 @@ mysql -u root --password="${mysql_pwd}" "${occ_db}" -N -s \ -e "DELETE FROM oc_accounts WHERE uid = '${uid}' LIMIT 1"; \ done ``` + +## Issues +Zimbra Drive is an official Zimbra product - support is provided through the official Zimbra channels to both Open Source and Network Edition customers. diff --git a/nextcloud-app/appinfo/info.xml b/nextcloud-app/appinfo/info.xml index e1e6079..7181a2b 100644 --- a/nextcloud-app/appinfo/info.xml +++ b/nextcloud-app/appinfo/info.xml @@ -28,7 +28,7 @@ AGPL ZeXtras - 0.8.18 + 0.8.23 ZimbraDrive auth integration @@ -42,7 +42,7 @@ - + diff --git a/nextcloud-app/appinfo/routes.php b/nextcloud-app/appinfo/routes.php index e2514bb..f42316b 100644 --- a/nextcloud-app/appinfo/routes.php +++ b/nextcloud-app/appinfo/routes.php @@ -29,8 +29,8 @@ return [ 'routes' => [ ['name' => 'page#index', 'url' => '/', 'verb' => 'GET'], - ['name' => 'zimbra_drive_api#searchRequest', 'url' => '/api/1.0/SearchRequest', 'verb' => 'POST'], - ['name' => 'zimbra_drive_api#getAllFolders', 'url' => '/api/1.0/GetAllFolders', 'verb' => 'POST'], + ['name' => 'zimbra_drive_api#searchRequestShareItemFiltered', 'url' => '/api/1.0/SearchRequest', 'verb' => 'POST'], + ['name' => 'zimbra_drive_api#getAllFoldersShareItemFiltered', 'url' => '/api/1.0/GetAllFolders', 'verb' => 'POST'], ['name' => 'zimbra_drive_api#getFile', 'url' => '/api/1.0/GetFile', 'verb' => 'POST'], ['name' => 'zimbra_drive_api#uploadFile', 'url' => '/api/1.0/UploadFile', 'verb' => 'POST'], ['name' => 'zimbra_drive_api#delete', 'url' => '/api/1.0/Delete', 'verb' => 'POST'], @@ -41,5 +41,9 @@ ['name' => 'admin_api#disableZimbraAuthentication', 'url' => '/admin/DisableZimbraAuthentication', 'verb' => 'POST'], ['name' => 'test#all', 'url' => '/test/All', 'verb' => 'GET'], ['name' => 'test#connectivityTest', 'url' => '/test/ConnectivityTest', 'verb' => 'GET'], + + ['name' => 'zimbra_drive_api#getVersion', 'url' => '/api/2.0/GetVersion', 'verb' => 'POST'], + ['name' => 'zimbra_drive_api#searchRequest', 'url' => '/api/2.0/SearchRequest', 'verb' => 'POST'], + ['name' => 'zimbra_drive_api#getAllFolders', 'url' => '/api/2.0/GetAllFolders', 'verb' => 'POST'], ] ]; diff --git a/nextcloud-app/js/admin.js b/nextcloud-app/js/admin.js index 153f308..dc56215 100644 --- a/nextcloud-app/js/admin.js +++ b/nextcloud-app/js/admin.js @@ -87,6 +87,10 @@ var documentsSettings = { documentsSettings.setValue('allow_zimbra_users_login', isEnabled); }, + setZimbraGroupToUsers: function (isEnabled) { + documentsSettings.setValue('set_zimbra_group', isEnabled); + }, + initialize: function () { setEnableZimbrasUsersUpdateHandler(); setAllowZimbrasUsersLoginUpdateHandler(); @@ -95,6 +99,7 @@ var documentsSettings = { setZimbraUrlChangeHandler(); setZimbraPortChangeHandler(); setPreAuthKeyChangeHandler(); + setZimbraGroupToUsersUpdateHandler(); function setEnableZimbrasUsersUpdateHandler() { @@ -145,6 +150,10 @@ var documentsSettings = { documentsSettings.setValue(this.name, this.value); } } + + function setZimbraGroupToUsersUpdateHandler() { + addClickHandlerToCheckableItem("set_zimbra_group", documentsSettings.setZimbraGroupToUsers); + } } }; diff --git a/nextcloud-app/lib/auth/abstractzimbrausersbackend.php b/nextcloud-app/lib/auth/abstractzimbrausersbackend.php index c20f2e6..969a6a0 100644 --- a/nextcloud-app/lib/auth/abstractzimbrausersbackend.php +++ b/nextcloud-app/lib/auth/abstractzimbrausersbackend.php @@ -33,6 +33,7 @@ abstract class AbstractZimbraUsersBackend extends RetroCompatibleBackend protected $userManager; protected $groupManager; protected $allow_zimbra_users_login; + protected $setZimbraGroupToUsers; /** @var AccountManager */ private $accountManager; /** @var ZimbraAuthenticationBackend */ @@ -72,6 +73,7 @@ public function __construct($server = null, $zimbraAuthenticationBackend = null) $appSettings = new AppSettings($this->config); $this->allow_zimbra_users_login = $appSettings->allowZimbraUsersLogin(); + $this->setZimbraGroupToUsers = $appSettings->setZimbraGroupToUsers(); } /** @@ -121,7 +123,10 @@ private function setDefaultUserAttributes($zimbraUser){ */ private function setDefaultGroups($user) { - $this->insertUserInGroup($user, self::ZIMBRA_GROUP); + if ($this->setZimbraGroupToUsers) + { + $this->insertUserInGroup($user, self::ZIMBRA_GROUP); + } $this->insertUserInGroup($user, $this->getEmailDomain($user->getEMailAddress())); } @@ -144,17 +149,33 @@ protected abstract function createUser($userId, $userDisplayName); */ private function restoreUserEmailIfChanged(User $user, $userEmail) { - if( $user->getEMailAddress() !== $userEmail) + if( $this->getUserEmailAddress($user) !== $userEmail) { - if(!is_null($this->accountManager)) //Nextcloud 11 - { - $userData = $this->accountManager->getUser($user); - $userData[AccountManager::PROPERTY_EMAIL]['value'] = $userEmail; - $this->accountManager->updateUser($user, $userData); - } else - { - $user->setEMailAddress($userEmail); - } + $this->setUserEmailAddress($user, $userEmail); + } + } + + private function getUserEmailAddress(User $user){ + if(!is_null($this->accountManager)) //Nextcloud 11 + { + $userData = $this->accountManager->getUser($user); + $userEmailAddress = $userData[AccountManager::PROPERTY_EMAIL]['value']; + } else + { + $userEmailAddress = $user->getEMailAddress(); + } + return $userEmailAddress; + } + + private function setUserEmailAddress(User $user, $userEmail){ + if(!is_null($this->accountManager)) //Nextcloud 11 + { + $userData = $this->accountManager->getUser($user); + $userData[AccountManager::PROPERTY_EMAIL]['value'] = $userEmail; + $this->accountManager->updateUser($user, $userData); + } else + { + $user->setEMailAddress($userEmail); } } diff --git a/nextcloud-app/lib/auth/zimbrausersbackend.php b/nextcloud-app/lib/auth/zimbrausersbackend.php index 9164edb..76d488e 100644 --- a/nextcloud-app/lib/auth/zimbrausersbackend.php +++ b/nextcloud-app/lib/auth/zimbrausersbackend.php @@ -41,7 +41,7 @@ public function __construct() private function initializeOcUserZimbraBackend() { if (class_exists('OC\\User\\Account')) { //ownCloud 10 all user backend will be 'degraded' to authentication backend - $this->oc_user_zimbra_backend = new ZimbraUsersBackendPassword(); + $this->oc_user_zimbra_backend = new ZimbraUsersBackendPassword($this); } else { $this->oc_user_zimbra_backend = new ZimbraUsersBackendInDb(); } diff --git a/nextcloud-app/lib/auth/zimbrausersbackendindb.php b/nextcloud-app/lib/auth/zimbrausersbackendindb.php index f4fe8ff..9ae8b44 100644 --- a/nextcloud-app/lib/auth/zimbrausersbackendindb.php +++ b/nextcloud-app/lib/auth/zimbrausersbackendindb.php @@ -215,7 +215,7 @@ protected function createUser($uid, $display_name) */ public function userExists($uid) { - $sql='SELECT COUNT(*) FROM `*PREFIX*zimbradrive_users`' + $sql='SELECT COUNT(*) AS users_find FROM `*PREFIX*zimbradrive_users`' . ' WHERE LOWER(`uid`) = LOWER(?)'; $statement = $this->databaseConnection->prepare($sql); @@ -224,8 +224,7 @@ public function userExists($uid) $row = $statement->fetch(); $statement->closeCursor(); - - return $row['COUNT(*)'] !== "0"; + return $row['users_find'] != 0; //$row['users_find'] in mysql is a string, in postgresql is a integer } } diff --git a/nextcloud-app/lib/auth/zimbrausersbackendpassword.php b/nextcloud-app/lib/auth/zimbrausersbackendpassword.php index d944d52..23700c3 100644 --- a/nextcloud-app/lib/auth/zimbrausersbackendpassword.php +++ b/nextcloud-app/lib/auth/zimbrausersbackendpassword.php @@ -24,6 +24,13 @@ class ZimbraUsersBackendPassword extends AbstractZimbraUsersBackend { + /** @var ZimbraUsersBackend */ + private $backend; + public function __construct($backend, $server = null, $zimbraAuthenticationBackend = null) + { + parent::__construct($server, $zimbraAuthenticationBackend); + $this->backend = $backend; + } /** * @param $userId @@ -33,7 +40,7 @@ protected function createUser($userId, $userDisplayName) { parent::__construct(); - $user = $this->userManager->createUser($userId, Random::string(255)); + $user = $this->userManager->createUserFromBackend($userId, Random::string(255), $this->backend); $user->setDisplayName($userDisplayName); } diff --git a/nextcloud-app/lib/controller/zimbradriveapicontroller.php b/nextcloud-app/lib/controller/zimbradriveapicontroller.php index 315eacf..7fdf8bf 100644 --- a/nextcloud-app/lib/controller/zimbradriveapicontroller.php +++ b/nextcloud-app/lib/controller/zimbradriveapicontroller.php @@ -86,9 +86,9 @@ public function __construct( * @param $caseSensitive bool * @return Response */ - public function searchRequest($username, $token, $query, $types, $caseSensitive) + public function searchRequestShareItemFiltered($username, $token, $query, $types, $caseSensitive) { - $this->logger->debug($username . ' call searchRequest.'); + $this->logger->debug($username . ' call searchRequestShareItemFiltered.'); try { $this->loginService->login($username, $token); } catch (UnauthorizedException $unauthorizedException) { @@ -119,6 +119,49 @@ public function searchRequest($username, $token, $query, $types, $caseSensitive) return new JSONResponse($resultsNoShares); } + /** + * @CORS + * @NoCSRFRequired + * @PublicPage + * @param $username + * @param $token + * @param $query + * @param $types + * @param $caseSensitive bool + * @return Response + */ + public function searchRequest($username, $token, $query, $types, $caseSensitive) + { + $this->logger->debug($username . ' call searchRequest.'); + try { + $this->loginService->login($username, $token); + } catch (UnauthorizedException $unauthorizedException) { + $this->logUnauthorizedLogin($unauthorizedException); + return new EmptyResponse(Http::STATUS_UNAUTHORIZED); + } + + $types = json_decode($types, false); + if($types === array('document')) + { + $types = array('file'); + } + $caseSensitive = $caseSensitive === "true"; + + try { + $wantedFiles = $this->searchService->search($query, $caseSensitive); + } catch (BadRequestException $badRequestException) { + $this->logger->info($badRequestException->getMessage()); + return new EmptyResponse(Http::STATUS_BAD_REQUEST); + } + catch (MethodNotAllowedException $methodNotAllowedException) { + $this->logger->info($methodNotAllowedException->getMessage()); + return new EmptyResponse(Http::STATUS_METHOD_NOT_ALLOWED); + } + + $results = $this->filterNodesByType($wantedFiles, $types); + return new JSONResponse($results); + } + /** * @param $nodes array * @param $allowedTypes array of string @@ -138,6 +181,45 @@ private function filterNodesByType($nodes, $allowedTypes) } + /** + * @param $node Node + * @param $validTypes array + * @return bool + */ + private function nodeHasAValidTypeShareItemFiltered($node, $validTypes) + { + return in_array($node[ResponseVarName::NODE_TYPE_VAR_NAME], $validTypes, true); + } + + /** + * @CORS + * @NoCSRFRequired + * @PublicPage + * @param $username + * @param $token + * @return \OCP\AppFramework\Http\Response + */ + public function getAllFoldersShareItemFiltered($username, $token) + { + $this->logger->debug($username . ' call getAllFoldersShareItemFiltered.'); + try { + $this->loginService->login($username, $token); + } catch (UnauthorizedException $unauthorizedException) { + $this->logUnauthorizedLogin($unauthorizedException); + return new EmptyResponse(Http::STATUS_UNAUTHORIZED); + } + + try { + $searchedFolder = $this->storageService->getFolder(StorageService::ROOT); + } catch (Exception $exception) { + $this->logger->info($exception->getMessage()); + return new EmptyResponse(Http::STATUS_FORBIDDEN); + } + $folderTree = $this->storageService->getFolderTreeAttributes($searchedFolder); + $folderTreeNoShare = $this->filterShareTreeNodes($folderTree); + return new JSONResponse($folderTreeNoShare); + } + /** * @param $node Node * @param $validTypes array @@ -173,8 +255,7 @@ public function getAllFolders($username, $token) return new EmptyResponse(Http::STATUS_FORBIDDEN); } $folderTree = $this->storageService->getFolderTreeAttributes($searchedFolder); - $folderTreeNoShare = $this->filterShareTreeNodes($folderTree); - return new JSONResponse($folderTreeNoShare); + return new JSONResponse($folderTree); } /** @@ -275,7 +356,7 @@ public function move($username, $token, $source_path, $target_path) catch (NotPermittedException $exception) { $this->logger->info($exception->getMessage()); - return new EmptyResponse(Http::STATUS_METHOD_NOT_ALLOWED); + return new EmptyResponse(Http::STATUS_FORBIDDEN); } // catch (Exception $exception) { // $this->logger->info($exception->getMessage()); diff --git a/nextcloud-app/lib/service/filter/directoryrootnodesfilter.php b/nextcloud-app/lib/service/filter/directoryrootnodesfilter.php index 0b9c79a..8dcd557 100644 --- a/nextcloud-app/lib/service/filter/directoryrootnodesfilter.php +++ b/nextcloud-app/lib/service/filter/directoryrootnodesfilter.php @@ -21,6 +21,7 @@ namespace OCA\ZimbraDrive\Service\Filter; use OCA\ZimbraDrive\Service\LogService; +use OCA\ZimbraDrive\Service\StorageService; use OCP\Files\Node; class DirectoryRootNodesFilter implements NodesFilter @@ -37,19 +38,25 @@ class DirectoryRootNodesFilter implements NodesFilter * @var LogService */ private $logger; + /** + * @var StorageService + */ + private $storageService; /** * DirectoryRootNodesFilter constructor. * @param $path string * @param $isCaseSensitive + * @param StorageService $storageService * @param LogService $logService */ - public function __construct($path, $isCaseSensitive, LogService $logService) + public function __construct($path, $isCaseSensitive, StorageService $storageService, LogService $logService) { $this->path = $path; $this->isCaseSensitive = $isCaseSensitive; $this->logger = $logService; + $this->storageService = $storageService; } /** @@ -62,8 +69,7 @@ public function filter($nodes) /** @var Node $node */ foreach($nodes as $node) { - $nodeInternalPath = $node->getInternalPath(); - $nodeUserRootRelativePath = substr($nodeInternalPath, strlen("files")); + $nodeUserRootRelativePath = $this->storageService->getRelativePath($node); if($this->isInTheDirectoryTree($nodeUserRootRelativePath, $this->path)) { $filteredNodes[] = $node; diff --git a/nextcloud-app/lib/service/filter/filterfactory.php b/nextcloud-app/lib/service/filter/filterfactory.php index d234528..ae5c66d 100644 --- a/nextcloud-app/lib/service/filter/filterfactory.php +++ b/nextcloud-app/lib/service/filter/filterfactory.php @@ -22,6 +22,7 @@ use OCA\ZimbraDrive\Service\LogService; +use OCA\ZimbraDrive\Service\StorageService; class FilterFactory { @@ -34,35 +35,42 @@ class FilterFactory * @var LogService */ private $logger; + /** + * @var StorageService + */ + private $storageService; /** * FilterFactory constructor. * @param FilterUtils $filterUtils * @param $isCaseSensitive + * @param StorageService $storageService * @param LogService $logService */ - public function __construct(FilterUtils $filterUtils, $isCaseSensitive, LogService $logService) + public function __construct(FilterUtils $filterUtils, $isCaseSensitive, StorageService $storageService, LogService $logService) { $this->isCaseSensitive = $isCaseSensitive; $this->filterUtils = $filterUtils; $this->logger = $logService; + $this->storageService = $storageService; } /** * @param $token * @return NodesFilter * @throws NoSuchFilterException + * @throws \OCA\ZimbraDrive\Service\BadRequestException */ public function createFilter($token) { if($this->filterUtils->queryIsFoldersContentsRequest($token)) { $searchRootPath = $this->filterUtils->assertPathFromToken($token); - return new DirectoryRootNodesFilter($searchRootPath, $this->isCaseSensitive, $this->logger); + return new DirectoryRootNodesFilter($searchRootPath, $this->isCaseSensitive, $this->storageService, $this->logger); } if($this->filterUtils->isPlainText($token)) { - return new PartialNameNodeFilter($token, $this->isCaseSensitive, $this->logger); + return new PartialNameNodeFilter($token, $this->isCaseSensitive, $this->storageService, $this->logger); } throw new NoSuchFilterException(); } diff --git a/nextcloud-app/lib/service/filter/filterfactoryprovider.php b/nextcloud-app/lib/service/filter/filterfactoryprovider.php index caad2ca..9acd0fb 100644 --- a/nextcloud-app/lib/service/filter/filterfactoryprovider.php +++ b/nextcloud-app/lib/service/filter/filterfactoryprovider.php @@ -22,6 +22,7 @@ namespace OCA\ZimbraDrive\Service\Filter; use OCA\ZimbraDrive\Service\LogService; +use OCA\ZimbraDrive\Service\StorageService; class FilterFactoryProvider { @@ -33,16 +34,22 @@ class FilterFactoryProvider * @var LogService */ private $logger; + /** + * @var StorageService + */ + private $storageService; /** * FilterFactoryProvider constructor. * @param FilterUtils $filterUtils + * @param StorageService $storageService * @param LogService $logService */ - public function __construct(FilterUtils $filterUtils, LogService $logService) + public function __construct(FilterUtils $filterUtils, StorageService $storageService, LogService $logService) { $this->filterUtils = $filterUtils; $this->logger = $logService; + $this->storageService = $storageService; } /** @@ -51,7 +58,7 @@ public function __construct(FilterUtils $filterUtils, LogService $logService) public function getCaseSensitiveFilterFactory() { $isCaseSensitive = true; - return new FilterFactory($this->filterUtils, $isCaseSensitive, $this->logger); + return new FilterFactory($this->filterUtils, $isCaseSensitive, $this->storageService, $this->logger); } @@ -61,6 +68,6 @@ public function getCaseSensitiveFilterFactory() public function getNonCaseSensitiveFilterFactory() { $isCaseSensitive = false; - return new FilterFactory($this->filterUtils, $isCaseSensitive, $this->logger); + return new FilterFactory($this->filterUtils, $isCaseSensitive, $this->storageService, $this->logger); } } \ No newline at end of file diff --git a/nextcloud-app/lib/service/filter/partialnamenodefilter.php b/nextcloud-app/lib/service/filter/partialnamenodefilter.php index 6c49372..46bc8c7 100644 --- a/nextcloud-app/lib/service/filter/partialnamenodefilter.php +++ b/nextcloud-app/lib/service/filter/partialnamenodefilter.php @@ -21,6 +21,7 @@ namespace OCA\ZimbraDrive\Service\Filter; use OCA\ZimbraDrive\Service\LogService; +use OCA\ZimbraDrive\Service\StorageService; use OCP\Files\Node; class PartialNameNodeFilter implements NodesFilter @@ -34,14 +35,19 @@ class PartialNameNodeFilter implements NodesFilter * @var LogService */ private $logger; + /** + * @var StorageService + */ + private $storageService; /** * PartialNameNodeFilter constructor. * @param $targetPartialName string * @param $isCaseSensitive + * @param StorageService $storageService * @param LogService $logService */ - public function __construct($targetPartialName, $isCaseSensitive, LogService $logService) + public function __construct($targetPartialName, $isCaseSensitive, StorageService $storageService, LogService $logService) { $this->logger = $logService; if(!$isCaseSensitive) @@ -50,6 +56,7 @@ public function __construct($targetPartialName, $isCaseSensitive, LogService $lo } $this->isCaseSensitive = $isCaseSensitive; $this->targetPartialName = $targetPartialName; + $this->storageService = $storageService; } @@ -59,7 +66,7 @@ public function filter($nodes) /** @var Node $node */ foreach($nodes as $node) { - $nodePath = $node->getInternalPath(); + $nodePath = $this->storageService->getInternalPath($node); $name = basename($nodePath); if(!$this->isCaseSensitive) diff --git a/nextcloud-app/lib/service/searchservice.php b/nextcloud-app/lib/service/searchservice.php index aad998c..8b32041 100644 --- a/nextcloud-app/lib/service/searchservice.php +++ b/nextcloud-app/lib/service/searchservice.php @@ -69,7 +69,7 @@ public function __construct(StorageService $storageService, LogService $logServi */ public function search($query, $isCaseSensitive) { - if($this->filterUtils->queryIsFoldersContentsRequest($query)) //actually only 'in:' is supports + if($this->filterUtils->queryIsFoldersContentsRequest($query)) // only 'in:' is supports { $path = $this->filterUtils->assertPathFromToken($query); diff --git a/nextcloud-app/lib/service/storageservice.php b/nextcloud-app/lib/service/storageservice.php index f2a9c0a..442a01c 100644 --- a/nextcloud-app/lib/service/storageservice.php +++ b/nextcloud-app/lib/service/storageservice.php @@ -24,6 +24,7 @@ use OCP\Files\Folder; use OCP\Files\File; use OCP\Files\IMimeTypeDetector; +use OCP\Files\NotPermittedException; use OCP\IServerContainer; use OC\Files\Filesystem; use OCP\Files\Node; @@ -148,7 +149,7 @@ public function getNodesCommonAttributes(Node $node) ResponseVarName::SIZE_VAR_NAME => $node->getSize(), ResponseVarName::MODIFIED_TIME_VAR_NAME => $node->getMTime(), ResponseVarName::ID_VAR_NAME => $node->getId(), - ResponseVarName::PATH_VAR_NAME => $node->getInternalPath(), + ResponseVarName::PATH_VAR_NAME => $this->getInternalPath($node), ResponseVarName::PUBLIC_VAR_NAME => $this->isPublic($node, $nodeOwner) ]; return $nodeAttributeMap; @@ -237,11 +238,18 @@ public function getSecureMimeType($path) * @param $sourcePath string * @param $targetPath string * @throws MethodNotAllowedException + * @throws NotPermittedException + * @throws UnauthorizedException + * @throws \OCP\Files\InvalidPathException + * @throws \OCP\Files\NotFoundException */ public function move($sourcePath, $targetPath) { //if the $targetPath is a directory, it use the $filePath file name $nodeToMove = $this->getNode($sourcePath); + if ( ($nodeToMove->getPermissions() & \OCP\Constants::PERMISSION_DELETE) === 0 ) { + throw new UnauthorizedException(); + } $lastCharTargetPath = $targetPath[strlen($targetPath)-1]; if($lastCharTargetPath === "/") { @@ -252,8 +260,7 @@ public function move($sourcePath, $targetPath) $targetFileName = basename($targetPath); $targetDirectory = $this->getFolder($targetDirectoryPath); $targetExists = $targetDirectory->nodeExists($targetFileName); - if($targetExists) - { + if($targetExists) { $errorMessage = "Cannot move the file because a file already exist in the destination path ($targetPath)."; throw new MethodNotAllowedException($errorMessage); } @@ -289,6 +296,7 @@ public function newDirectory($path) * @param $path * @param $tempFilePath * @throws MethodNotAllowedException + * @throws NotPermittedException */ public function uploadFile($name, $path, $tempFilePath) { @@ -303,7 +311,11 @@ public function uploadFile($name, $path, $tempFilePath) throw new MethodNotAllowedException($errorMessage); } - Filesystem::fromTmpFile($tempFilePath, $newFileFullPath); + $uploadResult = Filesystem::fromTmpFile($tempFilePath, $newFileFullPath); + + if ($uploadResult === false) { + throw new NotPermittedException(); + } } /** @@ -455,6 +467,26 @@ public function getFolderDescendants($folder) return $nodeDescendants; } + /** + * @param Node $node + * @return string + */ + public function getInternalPath(Node $node) + { + // strip everything "below" root folder from path + if ( $node->isMounted() || $node->isShared() ) + { + $nodePath = $node->getPath(); + $userRootFolder = $this->getFolder(self::ROOT)->getName(); + + $nodeInternalPath = substr( $nodePath, strpos($nodePath, '/'.$userRootFolder.'/')+1 ); + } else { + $nodeInternalPath = $node->getInternalPath(); + } + + return $nodeInternalPath; + } + /** * @param Node $node @@ -462,7 +494,7 @@ public function getFolderDescendants($folder) */ public function getRelativePath(Node $node) { - return substr($node->getInternalPath(), 5); //5 = length("files") + return substr($this->getInternalPath($node), 5); //5 = length("files") } /** @@ -481,4 +513,4 @@ public function isPublic($node, $owner) ); return count($shares) > 0; } -} \ No newline at end of file +} diff --git a/nextcloud-app/lib/settings/admintemplate.php b/nextcloud-app/lib/settings/admintemplate.php index 01ed4c6..2074453 100644 --- a/nextcloud-app/lib/settings/admintemplate.php +++ b/nextcloud-app/lib/settings/admintemplate.php @@ -59,7 +59,8 @@ public function getTemplate() AppSettings::TRUST_INVALID_CERTS => $this->appConfig->trustInvalidCertificatesDuringZimbraAuthentication(), AppSettings::PREAUTH_KEY => $this->appConfig->getZimbraPreauthKey(), AppSettings::ENABLE_ZIMBRA_USERS => $isUserBackEndOC_User_ZimbraDefined, - AppSettings::ALLOW_ZIMBRA_USERS_LOGIN => $this->appConfig->allowZimbraUsersLogin() + AppSettings::ALLOW_ZIMBRA_USERS_LOGIN => $this->appConfig->allowZimbraUsersLogin(), + AppSettings::SET_ZIMBRA_GROUP_TO_USERS => $this->appConfig->setZimbraGroupToUsers() ], 'blank' ); diff --git a/nextcloud-app/lib/settings/appsettings.php b/nextcloud-app/lib/settings/appsettings.php index db2e23f..5e9ea8c 100644 --- a/nextcloud-app/lib/settings/appsettings.php +++ b/nextcloud-app/lib/settings/appsettings.php @@ -33,6 +33,7 @@ class AppSettings const PREAUTH_KEY = "preauth_key"; const ALLOW_ZIMBRA_USERS_LOGIN = "allow_zimbra_users_login"; const ENABLE_ZIMBRA_USERS = "enable_zimbra_users"; + const SET_ZIMBRA_GROUP_TO_USERS = "set_zimbra_group"; /** @var IConfig */ private $config; @@ -78,4 +79,9 @@ public function allowZimbraUsersLogin() return $this->config->getAppValue(App::APP_NAME, self::ALLOW_ZIMBRA_USERS_LOGIN, 'false') === 'true'; } + public function setZimbraGroupToUsers() + { + return $this->config->getAppValue(App::APP_NAME, self::SET_ZIMBRA_GROUP_TO_USERS, 'true') === 'true'; + } + } \ No newline at end of file diff --git a/nextcloud-app/templates/admin.php b/nextcloud-app/templates/admin.php index 38f40ad..9037780 100644 --- a/nextcloud-app/templates/admin.php +++ b/nextcloud-app/templates/admin.php @@ -67,6 +67,11 @@ +
+ > + +
diff --git a/zimbra-extension/src/com/zextras/zimbradrive/CloudHttpRequestUtils.java b/zimbra-extension/src/com/zextras/zimbradrive/CloudHttpRequestUtils.java index 6440524..43643b9 100644 --- a/zimbra-extension/src/com/zextras/zimbradrive/CloudHttpRequestUtils.java +++ b/zimbra-extension/src/com/zextras/zimbradrive/CloudHttpRequestUtils.java @@ -38,8 +38,8 @@ public class CloudHttpRequestUtils { - private static final String DRIVE_ON_CLOUD_URL = "/apps/zimbradrive/api/1.0/"; - private static final String GET_FILE_URL = DRIVE_ON_CLOUD_URL + "GetFile"; + private static final String DRIVE_ON_CLOUD_URL = "/apps/zimbradrive/api/"; + private static final String GET_FILE_URL = DRIVE_ON_CLOUD_URL + "1.0/GetFile"; private final Provisioning mProvisioning; private final TokenManager mTokenManager; @@ -63,14 +63,18 @@ public List createDriveOnCloudAuthenticationParams(final ZimbraCo return driveOnCloudParameters; } - public HttpResponse sendRequestToCloud(final ZimbraContext zimbraContext, List driveOnCloudParameters, String driveCommand) + public HttpResponse sendRequestToCloud( + final ZimbraContext zimbraContext, + List driveOnCloudParameters, + String driveCommand, + String apiVersion) throws IOException { String authenticatedAccountId = zimbraContext.getAuthenticatedAccontId(); Account authenticatedUser = mProvisioning.assertAccountById(authenticatedAccountId); String userDomain = authenticatedUser.getDomainName(); String driveOnCloudDomain = mDriveProxy.getDriveDomainAssociatedToDomain(userDomain); - String searchRequestUrl = driveOnCloudDomain + DRIVE_ON_CLOUD_URL + driveCommand; + String searchRequestUrl = driveOnCloudDomain + DRIVE_ON_CLOUD_URL + apiVersion + "/" + driveCommand; HttpPost post = new HttpPost(searchRequestUrl); post.setEntity(BackendUtils.getEncodedForm(driveOnCloudParameters)); diff --git a/zimbra-extension/src/com/zextras/zimbradrive/soap/DeleteHdlr.java b/zimbra-extension/src/com/zextras/zimbradrive/soap/DeleteHdlr.java index a461cdf..dedbe2a 100644 --- a/zimbra-extension/src/com/zextras/zimbradrive/soap/DeleteHdlr.java +++ b/zimbra-extension/src/com/zextras/zimbradrive/soap/DeleteHdlr.java @@ -89,7 +89,8 @@ private void privateHandleRequest(ZimbraContext zimbraContext, SoapResponse soap private HttpResponse sendDeleteDriveOnCloudServerService(final ZimbraContext zimbraContext, final String targetPath) throws IOException { List driveOnCloudParameters = mCloudHttpRequestUtils.createDriveOnCloudAuthenticationParams(zimbraContext); driveOnCloudParameters.add(new BasicNameValuePair(ZimbraDriveItem.F_PATH, targetPath)); - return mCloudHttpRequestUtils.sendRequestToCloud(zimbraContext, driveOnCloudParameters, COMMAND); + return mCloudHttpRequestUtils.sendRequestToCloud(zimbraContext, driveOnCloudParameters, COMMAND, + "1.0"); } @Override diff --git a/zimbra-extension/src/com/zextras/zimbradrive/soap/GetAllFoldersHdlr.java b/zimbra-extension/src/com/zextras/zimbradrive/soap/GetAllFoldersHdlr.java index cd7f42e..99d9e8e 100644 --- a/zimbra-extension/src/com/zextras/zimbradrive/soap/GetAllFoldersHdlr.java +++ b/zimbra-extension/src/com/zextras/zimbradrive/soap/GetAllFoldersHdlr.java @@ -67,7 +67,8 @@ private void privateHandleRequest(ZimbraContext zimbraContext, SoapResponse soap private HttpResponse queryDriveOnCloudServerServiceFolder(final ZimbraContext zimbraContext) throws IOException { List driveOnCloudParameters = mCloudHttpRequestUtils.createDriveOnCloudAuthenticationParams(zimbraContext); - return mCloudHttpRequestUtils.sendRequestToCloud(zimbraContext, driveOnCloudParameters, COMMAND); + return mCloudHttpRequestUtils.sendRequestToCloud(zimbraContext, driveOnCloudParameters, COMMAND, + "2.0"); } private void appendSoapResponseFromDriveResponseFolder(final SoapResponse soapResponse, final String responseBody) diff --git a/zimbra-extension/src/com/zextras/zimbradrive/soap/MoveHdlr.java b/zimbra-extension/src/com/zextras/zimbradrive/soap/MoveHdlr.java index f4c0a3f..6fc6a21 100644 --- a/zimbra-extension/src/com/zextras/zimbradrive/soap/MoveHdlr.java +++ b/zimbra-extension/src/com/zextras/zimbradrive/soap/MoveHdlr.java @@ -74,7 +74,8 @@ private HttpResponse sendMoveToDriveOnCloudServerService(final ZimbraContext zim List driveOnCloudParameters = mCloudHttpRequestUtils.createDriveOnCloudAuthenticationParams(zimbraContext); driveOnCloudParameters.add(new BasicNameValuePair(ZimbraDriveItem.F_SOURCE_PATH, sourcePath)); driveOnCloudParameters.add(new BasicNameValuePair(ZimbraDriveItem.F_TARGET_PATH, targetPath)); - return mCloudHttpRequestUtils.sendRequestToCloud(zimbraContext, driveOnCloudParameters, COMMAND); + return mCloudHttpRequestUtils.sendRequestToCloud(zimbraContext, driveOnCloudParameters, COMMAND, + "1.0"); } @Override diff --git a/zimbra-extension/src/com/zextras/zimbradrive/soap/NewDirectoryHdlr.java b/zimbra-extension/src/com/zextras/zimbradrive/soap/NewDirectoryHdlr.java index a94c8c1..dd371a6 100644 --- a/zimbra-extension/src/com/zextras/zimbradrive/soap/NewDirectoryHdlr.java +++ b/zimbra-extension/src/com/zextras/zimbradrive/soap/NewDirectoryHdlr.java @@ -87,7 +87,8 @@ private void appendSoapResponseFromDriveResponseFolder(final SoapResponse soapRe private HttpResponse sendNewDirectoryToDriveOnCloudServerService(final ZimbraContext zimbraContext, final String path) throws IOException { List driveOnCloudParameters = mCloudHttpRequestUtils.createDriveOnCloudAuthenticationParams(zimbraContext); driveOnCloudParameters.add(new BasicNameValuePair(ZimbraDriveItem.F_PATH, path)); - return mCloudHttpRequestUtils.sendRequestToCloud(zimbraContext, driveOnCloudParameters, COMMAND); + return mCloudHttpRequestUtils.sendRequestToCloud(zimbraContext, driveOnCloudParameters, COMMAND, + "1.0"); } @Override diff --git a/zimbra-extension/src/com/zextras/zimbradrive/soap/SearchRequestHdlr.java b/zimbra-extension/src/com/zextras/zimbradrive/soap/SearchRequestHdlr.java index 63ede97..ac32106 100644 --- a/zimbra-extension/src/com/zextras/zimbradrive/soap/SearchRequestHdlr.java +++ b/zimbra-extension/src/com/zextras/zimbradrive/soap/SearchRequestHdlr.java @@ -128,7 +128,8 @@ private HttpResponse queryDriveOnCloudServerService(final ZimbraContext zimbraCo driveOnCloudParameters.add(new BasicNameValuePair("query", query)); driveOnCloudParameters.add(new BasicNameValuePair("types", types)); driveOnCloudParameters.add(new BasicNameValuePair("caseSensitive", isCaseSensitive.toString())); - return mCloudHttpRequestUtils.sendRequestToCloud(zimbraContext, driveOnCloudParameters, COMMAND + "Request"); + return mCloudHttpRequestUtils.sendRequestToCloud(zimbraContext, driveOnCloudParameters, COMMAND + "Request", + "2.0"); } @Override diff --git a/zimbra-extension/version b/zimbra-extension/version index 38f77a6..50ffc5a 100644 --- a/zimbra-extension/version +++ b/zimbra-extension/version @@ -1 +1 @@ -2.0.1 +2.0.3 diff --git a/zimlet/i18n/com_zextras_drive_open.properties b/zimlet/i18n/com_zextras_drive_open.properties index 01b607a..fc2423f 100644 --- a/zimlet/i18n/com_zextras_drive_open.properties +++ b/zimlet/i18n/com_zextras_drive_open.properties @@ -1,6 +1,6 @@ zimletLabel=Zimbra Drive zimletDescription=Cloud integration for Zimbra -tabName=Drive +tabName=Open Drive searchZimbraDrive=File on ZDrive zimbraDriveFolders=Folders downloadFolder=Download Folder @@ -18,3 +18,6 @@ errorUploadFileAlreadyExists=Some files already exist({0}). errorUploadFileUploadNotPermitted=You have no permissions to upload some files({0}). errorUploadFileAlreadyExistsGeneric=Some files already exist. errorRenameFile={0} already exists in destination Drive folder. +errorCreateNewFolder=Error during the creation of the new folder +errorUpload=Error during the upload of the file +errorDelete=Error during the delete of the file diff --git a/zimlet/package.json b/zimlet/package.json index 65101c8..8779016 100644 --- a/zimlet/package.json +++ b/zimlet/package.json @@ -1,6 +1,6 @@ { "name": "com-zextras-zimbra-drive", - "version": "2.0.1", + "version": "2.0.4", "description": "A Zimlet for Zimbra", "main": "./src/com_zextras_drive_open_hdlr.ts", "scripts": { diff --git a/zimlet/src/ZimbraDriveController.ts b/zimlet/src/ZimbraDriveController.ts index d27b62a..096a2b2 100644 --- a/zimlet/src/ZimbraDriveController.ts +++ b/zimlet/src/ZimbraDriveController.ts @@ -581,7 +581,8 @@ export class ZimbraDriveController extends ZmListController { (appCtxt.getAppController()).sendRequest({ soapDoc: soapDoc, asyncMode: true, - callback: new AjxCallback(this, this._onDeleteDone, [items]) + callback: new AjxCallback(this, this._onDeleteDone, [items]), + errorCallback: new AjxCallback(this, this._onDeleteError), }); } @@ -610,6 +611,14 @@ export class ZimbraDriveController extends ZmListController { } } + private _onDeleteError(): boolean { + appCtxt.setStatusMsg({ + msg: ZimbraDriveApp.getMessage("errorDelete"), + level: ZmStatusView.LEVEL_WARNING, + }); + return true; + } + private _renameFileListener(ev: DwtUiEvent): void { let items: ZimbraDriveItem[] = this._listView[this._currentViewId].getSelection(); if (!items) { return; } diff --git a/zimlet/src/ZimbraDriveTreeController.ts b/zimlet/src/ZimbraDriveTreeController.ts index 55707de..1a2bcbb 100644 --- a/zimlet/src/ZimbraDriveTreeController.ts +++ b/zimlet/src/ZimbraDriveTreeController.ts @@ -43,7 +43,6 @@ import {ZmCsfeResult} from "./zimbra/zimbra/csfe/ZmCsfeResult"; import {DwtDropEvent} from "./zimbra/ajax/dwt/dnd/DwtDropEvent"; import {ZimbraDriveController} from "./ZimbraDriveController"; import {ZDId} from "./ZDId"; -import {AjxMessageFormat} from "./zimbra/ajax/util/AjxText"; import {AjxStringUtil} from "./zimbra/ajax/util/AjxStringUtil"; import {PreviewPaneView} from "./view/PreviewPaneView"; import {DwtTree} from "./zimbra/ajax/dwt/widgets/DwtTree"; @@ -52,7 +51,6 @@ import {ZmFolderSearchFilterGetMoveParamsValue} from "./zimbra/zimbraMail/share/ import {ZmOverview} from "./zimbra/zimbraMail/share/view/ZmOverview"; import {ZmStatusView} from "./zimbra/zimbraMail/share/view/ZmStatusView"; import {ZmCsfeException} from "./zimbra/zimbra/csfe/ZmCsfeException"; -import {ZimbraDriveFolderTree} from "./ZimbraDriveFolderTree"; import {ZmFolderTreeController} from "./zimbra/zimbraMail/share/controller/ZmFolderTreeController"; import {ZmOrganizer} from "./zimbra/zimbraMail/share/model/ZmOrganizer"; import {DwtHeaderTreeItem} from "./zimbra/ajax/dwt/widgets/DwtHeaderTreeItem"; @@ -175,10 +173,19 @@ export class ZimbraDriveTreeController extends ZmFolderTreeController { soapDoc: soapDoc, asyncMode: true, callback: new AjxCallback(this, this._newFolderCallback, [this._newFolderDialog.getFolder()]), - errorCallback: new AjxCallback(this, this._newFolderDialog.popdown, []) + errorCallback: new AjxCallback(this, this._createFolderError, []) }); } + private _createFolderError = (): boolean => { + this._newFolderDialog.popdown(); + appCtxt.setStatusMsg({ + msg: ZimbraDriveApp.getMessage("errorCreateNewFolder"), + level: ZmStatusView.LEVEL_WARNING, + }); + return true; + } + private _newFolderCallback(parentFolder: ZimbraDriveFolder, result: ZmCsfeResult): void { let newFolder: ZimbraDriveFolder = new ZimbraDriveFolder(); let currentController: ZimbraDriveController = appCtxt.getCurrentController(); diff --git a/zimlet/src/i18n/com_zextras_drive_open.properties b/zimlet/src/i18n/com_zextras_drive_open.properties index 0895a2a..296c861 100644 --- a/zimlet/src/i18n/com_zextras_drive_open.properties +++ b/zimlet/src/i18n/com_zextras_drive_open.properties @@ -17,7 +17,7 @@ zimletLabel=Zimbra Drive zimletDescription=Cloud integration for Zimbra -tabName=Drive +tabName=Open Drive retry=Retry searchZimbraDrive=File on ZDrive zimbraDriveFolder=Drive Folder @@ -42,4 +42,7 @@ errorUploadFileUploadNotPermitted=You have no permissions to upload some files({ errorUploadFileAlreadyExistsGeneric=Some files already exist. errorRenameFile={0} already exists in destination Drive folder. errorDeletingRootFolder=Root folder cannot be deleted. -rootName=Drive \ No newline at end of file +rootName=Drive +errorCreateNewFolder=Error during the creation of the new folder +errorUpload=Error during the upload of the file +errorDelete=Error during the delete of the file \ No newline at end of file diff --git a/zimlet/src/view/DetailListView.ts b/zimlet/src/view/DetailListView.ts index 65cac28..fa61231 100644 --- a/zimlet/src/view/DetailListView.ts +++ b/zimlet/src/view/DetailListView.ts @@ -38,6 +38,7 @@ import {DwtDragEvent} from "../zimbra/ajax/dwt/dnd/DwtDragEvent"; import {DwtDropEvent} from "../zimbra/ajax/dwt/dnd/DwtDropEvent"; import {AjxMessageFormat} from "../zimbra/ajax/util/AjxText"; import {AjxEnv} from "../zimbra/ajax/boot/AjxEnv"; +import {AjxStringUtil} from "../zimbra/ajax/util/AjxStringUtil"; export class DetailListView extends ZimbraDriveBaseView { @@ -183,7 +184,7 @@ export class DetailListView extends ZimbraDriveBaseView { } else if (field === ZimbraDriveItem.F_DATE) { htmlArr[idx++] = ""; } else if (field === ZimbraDriveItem.F_FROM) { - htmlArr[idx++] = zimbraDriveItem.getAuthor(); + htmlArr[idx++] = AjxStringUtil.htmlEncode(zimbraDriveItem.getAuthor()); } else if (field === ZimbraDriveItem.F_FOLDER) { zimbraDriveItem.setParentNameElId(this._getFieldId(zimbraDriveItem, ZimbraDriveItem.F_FOLDER)); htmlArr[idx++] = "
" + zimbraDriveItem.getParentName() + "
"; @@ -206,7 +207,7 @@ export class DetailListView extends ZimbraDriveBaseView { } else if (field === ZimbraDriveItem.F_DATE) { htmlArr[idx++] = AjxDateUtil.simpleComputeDateStr(new Date(zimbraDriveItem.getModifiedTimeMillis())); } else if (field === ZimbraDriveItem.F_FROM) { - htmlArr[idx++] = zimbraDriveItem.getAuthor(); + htmlArr[idx++] = AjxStringUtil.htmlEncode(zimbraDriveItem.getAuthor()); } else if (field === ZimbraDriveItem.F_FOLDER) { zimbraDriveItem.setParentNameElId(this._getFieldId(zimbraDriveItem, ZimbraDriveItem.F_FOLDER)); htmlArr[idx++] = "
" + zimbraDriveItem.getParentName() + "
"; diff --git a/zimlet/src/view/PreviewView.ts b/zimlet/src/view/PreviewView.ts index 180e280..194bbed 100644 --- a/zimlet/src/view/PreviewView.ts +++ b/zimlet/src/view/PreviewView.ts @@ -229,7 +229,7 @@ export class PreviewView extends DwtComposite { this.getElement("modified").innerHTML = dateFormatter.format(new Date(item.getModifiedTimeMillis())); } if (this.getElement("creator")) - this.getElement("creator").innerHTML = item.getAuthor(); + this.getElement("creator").innerHTML = AjxStringUtil.htmlEncode(item.getAuthor()); if (this.getElement("lock")) this.getElement("lock").innerHTML = AjxImg.getImageHtml(!item.getPermissions().writable ? "Padlock" : "Blank_16"); @@ -278,7 +278,7 @@ export class PreviewView extends DwtComposite { this.getElement("name").innerHTML = AjxStringUtil.htmlEncode(item.getName()); this.getElement("image").className = "ImgBriefcase_48"; if (this.getElement("modifier")) - this.getElement("modifier").innerHTML = item.getAuthor(); + this.getElement("modifier").innerHTML = AjxStringUtil.htmlEncode(item.getAuthor()); this._setIframeContent(AjxTemplate.expand("briefcase.Briefcase#FolderPreview")); } diff --git a/zimlet/src/view/ZimbraDriveUploadDialog.ts b/zimlet/src/view/ZimbraDriveUploadDialog.ts index 22ffd91..b2f5193 100644 --- a/zimlet/src/view/ZimbraDriveUploadDialog.ts +++ b/zimlet/src/view/ZimbraDriveUploadDialog.ts @@ -158,7 +158,7 @@ export class ZimbraDriveUploadDialog extends ZmUploadDialog { alreadyExistsFiles.push(this._fileMapIdName[id]); } } - else if (status === ZimbraDriveUploadDialog.FILE_ALREADY_EXISTS_STATUS_CODE) { + else if (status === ZimbraDriveUploadDialog.NOT_PERMITTED_EXCEPTION_STATUS_CODE) { if (this._fileMapIdName.hasOwnProperty(id)) { filesUploadNotPermitted.push(this._fileMapIdName[id]); } @@ -182,7 +182,7 @@ export class ZimbraDriveUploadDialog extends ZmUploadDialog { level = ZmStatusView.LEVEL_WARNING; } else if (filesUploadNotPermitted.length > 0) { - ZimbraDriveApp.getMessage("errorUploadFileUploadNotPermitted", [filesUploadNotPermitted.join(", ")]); + msg = ZimbraDriveApp.getMessage("errorUploadFileUploadNotPermitted", [filesUploadNotPermitted.join(", ")]); level = ZmStatusView.LEVEL_WARNING; } break;