diff --git a/frontend/package-lock.json b/frontend/package-lock.json index c3e05ded43..acec21e9db 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -433,9 +433,9 @@ "integrity": "sha512-YsAX+gnnf1ytv7asZgJP7T56DALQniKtRVtlz0f11PljLV19I1Av+Oz3QcYaRiKhCCB+EMnVKI9Yc14sYKp6lA==" }, "@seafile/seafile-editor": { - "version": "0.2.37", - "resolved": "https://registry.npmjs.org/@seafile/seafile-editor/-/seafile-editor-0.2.37.tgz", - "integrity": "sha512-53Md4MNqhw4GmFRyZRsWH1pK9S+sHoaAjbqLW6cMdRtMzxQTBM76pqXSxhVuP7TuGX7f1tLN7piXdVzjZuZVzA==", + "version": "0.2.40", + "resolved": "https://registry.npmjs.org/@seafile/seafile-editor/-/seafile-editor-0.2.40.tgz", + "integrity": "sha512-6xBlf06sF1o802aRAdps9tWE02qpl2Dd0x3s7aYTxp8MoN+2/S5Tqt85Y4fOWOt0ZSvAaK48K7v7DCVoW3jKMQ==", "requires": { "@seafile/slate-react": "^0.1.8", "autoprefixer": "7.1.6", @@ -614,9 +614,9 @@ } }, "@types/node": { - "version": "12.0.8", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.0.8.tgz", - "integrity": "sha512-b8bbUOTwzIY3V5vDTY1fIJ+ePKDUBqt2hC2woVGotdQQhG/2Sh62HOKHrT7ab+VerXAcPyAiTEipPu/FsreUtg==" + "version": "12.0.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.0.10.tgz", + "integrity": "sha512-LcsGbPomWsad6wmMNv7nBLw7YYYyfdYcz6xryKYQhx89c3XXan+8Q6AJ43G5XDIaklaVkK3mE4fCb0SBvMiPSQ==" }, "@videojs/http-streaming": { "version": "1.5.1", @@ -15921,9 +15921,9 @@ } }, "seafile-js": { - "version": "0.2.92", - "resolved": "https://registry.npmjs.org/seafile-js/-/seafile-js-0.2.92.tgz", - "integrity": "sha512-GySMa7cr8LXWj+uyIOG9JAIzUlzuGHZ1dLbx0tKrUcJ9vY10xVnDPCmJz3jEnG59wXNSlqkwcEaQxvaANfdgeg==", + "version": "0.2.93", + "resolved": "https://registry.npmjs.org/seafile-js/-/seafile-js-0.2.93.tgz", + "integrity": "sha512-5stGnu5LJZj2MoN9ABjh/iYBpQpF5skbxmXZH4w4Vb1GVeQOWFz7oGWe0pSMMZvKriXN6GNRHB0K1nkL+OFBAg==", "requires": { "axios": "^0.18.0", "form-data": "^2.3.2", diff --git a/frontend/package.json b/frontend/package.json index c904859235..5b4cd9e193 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -6,7 +6,7 @@ "@reach/router": "^1.2.0", "@seafile/dtable": "0.0.37", "@seafile/resumablejs": "^1.1.9", - "@seafile/seafile-editor": "^0.2.37", + "@seafile/seafile-editor": "^0.2.40", "MD5": "^1.3.0", "autoprefixer": "7.1.6", "classnames": "^2.2.6", @@ -37,7 +37,7 @@ "react-responsive": "^6.1.1", "react-select": "^2.4.1", "reactstrap": "^6.4.0", - "seafile-js": "^0.2.92", + "seafile-js": "^0.2.93", "socket.io-client": "^2.2.0", "sw-precache-webpack-plugin": "0.11.4", "unified": "^7.0.0", diff --git a/frontend/src/components/dir-view-mode/dir-column-view.js b/frontend/src/components/dir-view-mode/dir-column-view.js index 65fa1c7017..2c8df33f6d 100644 --- a/frontend/src/components/dir-view-mode/dir-column-view.js +++ b/frontend/src/components/dir-view-mode/dir-column-view.js @@ -11,7 +11,7 @@ const propTypes = { currentRepoInfo: PropTypes.object.isRequired, repoPermission: PropTypes.bool.isRequired, enableDirPrivateShare: PropTypes.bool.isRequired, - userPrem: PropTypes.bool, + userPerm: PropTypes.string, isGroupOwnedRepo: PropTypes.bool.isRequired, // tree isTreeDataLoading: PropTypes.bool.isRequired, @@ -46,7 +46,6 @@ const propTypes = { // list isDirentListLoading: PropTypes.bool.isRequired, direntList: PropTypes.array.isRequired, - showShareBtn: PropTypes.bool.isRequired, sortBy: PropTypes.string.isRequired, sortOrder: PropTypes.string.isRequired, sortItems: PropTypes.func.isRequired, @@ -197,6 +196,7 @@ class DirColumnView extends React.Component { repoID={this.props.repoID} currentRepoInfo={this.props.currentRepoInfo} isGroupOwnedRepo={this.props.isGroupOwnedRepo} + userPerm={this.props.userPerm} enableDirPrivateShare={this.props.enableDirPrivateShare} isRepoInfoBarShow={this.props.isRepoInfoBarShow} usedRepoTags={this.props.usedRepoTags} @@ -205,7 +205,6 @@ class DirColumnView extends React.Component { updateUsedRepoTags={this.props.updateUsedRepoTags} isDirentListLoading={this.props.isDirentListLoading} direntList={this.props.direntList} - showShareBtn={this.props.showShareBtn} sortBy={this.props.sortBy} sortOrder={this.props.sortOrder} sortItems={this.props.sortItems} diff --git a/frontend/src/components/dir-view-mode/dir-grid-view.js b/frontend/src/components/dir-view-mode/dir-grid-view.js index 2dff26c215..75010029b5 100644 --- a/frontend/src/components/dir-view-mode/dir-grid-view.js +++ b/frontend/src/components/dir-view-mode/dir-grid-view.js @@ -21,12 +21,12 @@ const propTypes = { onItemCopy: PropTypes.func.isRequired, onRenameNode: PropTypes.func.isRequired, isGroupOwnedRepo: PropTypes.bool.isRequired, + userPerm: PropTypes.string, isRepoInfoBarShow: PropTypes.bool.isRequired, isDirentListLoading: PropTypes.bool.isRequired, isDirentDetailShow: PropTypes.bool.isRequired, enableDirPrivateShare: PropTypes.bool.isRequired, updateDirent: PropTypes.func.isRequired, - showShareBtn: PropTypes.bool.isRequired, showDirentDetail: PropTypes.func.isRequired, onAddFolder: PropTypes.func.isRequired, onFileTagChanged: PropTypes.func, @@ -63,6 +63,7 @@ class DirGridView extends React.Component { repoID={this.props.repoID} currentRepoInfo={this.props.currentRepoInfo} isGroupOwnedRepo={this.props.isGroupOwnedRepo} + userPerm={this.props.userPerm} enableDirPrivateShare={this.props.enableDirPrivateShare} direntList={this.props.direntList} onAddFile={this.props.onAddFile} @@ -72,7 +73,6 @@ class DirGridView extends React.Component { onItemCopy={this.props.onItemCopy} isDirentListLoading={this.props.isDirentListLoading} updateDirent={this.props.updateDirent} - showShareBtn={this.props.showShareBtn} onRenameNode={this.props.onRenameNode} showDirentDetail={this.props.showDirentDetail} onGridItemClick={this.props.onGridItemClick} diff --git a/frontend/src/components/dir-view-mode/dir-list-view.js b/frontend/src/components/dir-view-mode/dir-list-view.js index 95f5bbea60..ac089197d2 100644 --- a/frontend/src/components/dir-view-mode/dir-list-view.js +++ b/frontend/src/components/dir-view-mode/dir-list-view.js @@ -9,6 +9,7 @@ const propTypes = { repoID: PropTypes.string.isRequired, currentRepoInfo: PropTypes.object.isRequired, isGroupOwnedRepo: PropTypes.bool.isRequired, + userPerm: PropTypes.string, enableDirPrivateShare: PropTypes.bool.isRequired, isRepoInfoBarShow: PropTypes.bool.isRequired, usedRepoTags: PropTypes.array.isRequired, @@ -17,7 +18,6 @@ const propTypes = { updateUsedRepoTags: PropTypes.func.isRequired, isDirentListLoading: PropTypes.bool.isRequired, direntList: PropTypes.array.isRequired, - showShareBtn: PropTypes.bool.isRequired, sortBy: PropTypes.string.isRequired, sortOrder: PropTypes.string.isRequired, sortItems: PropTypes.func.isRequired, @@ -73,9 +73,9 @@ class DirListView extends React.Component { currentRepoInfo={this.props.currentRepoInfo} repoID={this.props.repoID} isGroupOwnedRepo={this.props.isGroupOwnedRepo} + userPerm={this.props.userPerm} enableDirPrivateShare={this.props.enableDirPrivateShare} direntList={this.props.direntList} - showShareBtn={this.props.showShareBtn} sortBy={this.props.sortBy} sortOrder={this.props.sortOrder} sortItems={this.props.sortItems} diff --git a/frontend/src/components/dirent-grid-view/dirent-grid-view.js b/frontend/src/components/dirent-grid-view/dirent-grid-view.js index f808cce064..2a1308eda8 100644 --- a/frontend/src/components/dirent-grid-view/dirent-grid-view.js +++ b/frontend/src/components/dirent-grid-view/dirent-grid-view.js @@ -34,7 +34,7 @@ const propTypes = { onItemClick: PropTypes.func.isRequired, isDirentListLoading: PropTypes.bool.isRequired, isGroupOwnedRepo: PropTypes.bool.isRequired, - showShareBtn: PropTypes.bool.isRequired, + userPerm: PropTypes.string, // current path's user permission enableDirPrivateShare: PropTypes.bool.isRequired, updateDirent: PropTypes.func.isRequired, isDirentDetailShow: PropTypes.bool.isRequired, @@ -354,12 +354,19 @@ class DirentGridView extends React.Component{ } onGridContainerContextMenu = (event) => { + event.preventDefault(); + // Display menu items based on the permissions of the current path + let permission = this.props.userPerm; + if (permission !== 'admin' && permission !== 'rw') { + return; + } let id = 'dirent-grid-container-menu'; let menuList = [TextTranslation.NEW_FOLDER, TextTranslation.NEW_FILE]; this.handleContextClick(event, id, menuList); } onGridItemContextMenu = (event, dirent) => { + // Display menu items according to the current dirent permission let id = 'grid-item-contextmenu'; let menuList = this.getDirentItemMenuList(dirent, true); this.handleContextClick(event, id, menuList, dirent); @@ -407,12 +414,15 @@ class DirentGridView extends React.Component{ let type = dirent.type; let permission = dirent.permission; - + let showShareBtn = Utils.isHasPermissionToShare(currentRepoInfo, permission, dirent); let menuList = []; let contextmenuList = []; if (isContextmenu) { let { SHARE, DOWNLOAD, DELETE } = TextTranslation; - contextmenuList = this.props.showShareBtn ? [SHARE] : []; + + if (showShareBtn) { + contextmenuList = [SHARE]; + } if (dirent.permission === 'rw' || dirent.permission === 'r') { contextmenuList = [...contextmenuList, DOWNLOAD]; @@ -422,7 +432,7 @@ class DirentGridView extends React.Component{ contextmenuList = [...contextmenuList, DELETE]; } - contextmenuList = [...contextmenuList, 'Divider']; + contextmenuList = contextmenuList.length > 0 ? [...contextmenuList, 'Divider'] : []; } let { RENAME, MOVE, COPY, PERMISSION, OPEN_VIA_CLIENT, LOCK, UNLOCK, COMMENT, HISTORY, ACCESS_LOG } = TextTranslation; @@ -436,7 +446,8 @@ class DirentGridView extends React.Component{ } if (type === 'dir' && permission === 'r') { - menuList = currentRepoInfo.encrypted ? [...contextmenuList, COPY] : []; + menuList = [...contextmenuList]; + menuList = currentRepoInfo.encrypted ? [...menuList, COPY] : menuList.slice(0, menuList.length - 1); return menuList; } diff --git a/frontend/src/components/dirent-list-view/dirent-list-item.js b/frontend/src/components/dirent-list-view/dirent-list-item.js index cde51c86bf..fd43faa698 100644 --- a/frontend/src/components/dirent-list-view/dirent-list-item.js +++ b/frontend/src/components/dirent-list-view/dirent-list-item.js @@ -22,7 +22,6 @@ const propTypes = { path: PropTypes.string.isRequired, repoID: PropTypes.string.isRequired, isItemFreezed: PropTypes.bool.isRequired, - showShareBtn: PropTypes.bool.isRequired, dirent: PropTypes.object.isRequired, onItemClick: PropTypes.func.isRequired, freezeItem: PropTypes.func.isRequired, @@ -375,11 +374,12 @@ class DirentListItem extends React.Component { e.dataTransfer.setData('applicaiton/drag-item-info', dragStartItemData); } - onItemDragEnter = () => { + onItemDragEnter = (e) => { if (Utils.isIEBrower()) { return false; } if (this.props.dirent.type === 'dir') { + e.stopPropagation(); this.setState({isDropTipshow: true}); } } @@ -392,10 +392,14 @@ class DirentListItem extends React.Component { e.dataTransfer.dropEffect = 'move'; } - onItemDragLeave = () => { + onItemDragLeave = (e) => { if (Utils.isIEBrower()) { return false; } + + if (this.props.dirent.type === 'dir') { + e.stopPropagation(); + } this.setState({isDropTipshow: false}); } @@ -407,6 +411,9 @@ class DirentListItem extends React.Component { if (e.dataTransfer.files.length) { // uploaded files return; } + if (this.props.dirent.type === 'dir') { + e.stopPropagation(); + } let dragStartItemData = e.dataTransfer.getData('applicaiton/drag-item-info'); dragStartItemData = JSON.parse(dragStartItemData); if (Array.isArray(dragStartItemData)) { //move items @@ -458,10 +465,11 @@ class DirentListItem extends React.Component { } renderItemOperation = () => { - let { dirent, selectedDirentList, currentRepoInfo, showShareBtn } = this.props; + let { dirent, selectedDirentList, currentRepoInfo } = this.props; if (currentRepoInfo.permission === 'cloud-edit' || currentRepoInfo.permission === 'preview') { return ''; } + let showShareBtn = Utils.isHasPermissionToShare(currentRepoInfo, dirent.permission, dirent); return ( diff --git a/frontend/src/components/dirent-list-view/dirent-list-view.js b/frontend/src/components/dirent-list-view/dirent-list-view.js index a71524cd6c..317d8f24dc 100644 --- a/frontend/src/components/dirent-list-view/dirent-list-view.js +++ b/frontend/src/components/dirent-list-view/dirent-list-view.js @@ -25,7 +25,6 @@ const propTypes = { isAllItemSelected: PropTypes.bool.isRequired, isDirentListLoading: PropTypes.bool.isRequired, direntList: PropTypes.array.isRequired, - showShareBtn: PropTypes.bool.isRequired, sortBy: PropTypes.string.isRequired, sortOrder: PropTypes.string.isRequired, sortItems: PropTypes.func.isRequired, @@ -47,6 +46,7 @@ const propTypes = { onFileTagChanged: PropTypes.func, enableDirPrivateShare: PropTypes.bool.isRequired, isGroupOwnedRepo: PropTypes.bool.isRequired, + userPerm: PropTypes.string, showDirentDetail: PropTypes.func.isRequired, }; @@ -71,6 +71,7 @@ class DirentListView extends React.Component { isListDropTipShow: false, }; + this.enteredCounter = 0; // Determine whether to enter the child element to avoid dragging bubbling bugs。 this.isRepoOwner = props.currentRepoInfo.owner_email === username; this.isAdmin = props.currentRepoInfo.is_admin; this.repoEncrypted = props.currentRepoInfo.encrypted; @@ -294,12 +295,6 @@ class DirentListView extends React.Component { event.preventDefault(); event.stopPropagation(); - let currentRepoInfo = this.props.currentRepoInfo; - - if (currentRepoInfo.permission === 'cloud-edit' || currentRepoInfo.permission === 'preview') { - return ''; - } - let x = event.clientX || (event.touches && event.touches[0].pageX); let y = event.clientY || (event.touches && event.touches[0].pageY); @@ -339,6 +334,13 @@ class DirentListView extends React.Component { } onContainerContextMenu = (event) => { + event.preventDefault(); + // Display menu items based on the permissions of the current path + let permission = this.props.userPerm; + if (permission !== 'admin' && permission !== 'rw') { + return; + } + if (this.props.selectedDirentList.length === 0) { let id = 'dirent-container-menu'; let menuList = [TextTranslation.NEW_FOLDER, TextTranslation.NEW_FILE]; @@ -419,6 +421,7 @@ class DirentListView extends React.Component { } onItemContextMenu = (event, dirent) => { + // Display menu items according to the current dirent permission if (this.props.selectedDirentList.length > 1) { return; } @@ -461,20 +464,23 @@ class DirentListView extends React.Component { } getDirentItemMenuList = (dirent, isContextmenu) => { - let isRepoOwner = this.isRepoOwner; let currentRepoInfo = this.props.currentRepoInfo; let can_set_folder_perm = folderPermEnabled && ((isRepoOwner && currentRepoInfo.has_been_shared_out) || currentRepoInfo.is_admin); let type = dirent.type; let permission = dirent.permission; + let showShareBtn = Utils.isHasPermissionToShare(currentRepoInfo, permission, dirent); let menuList = []; let contextmenuList = []; if (isContextmenu) { let { SHARE, DOWNLOAD, DELETE } = TextTranslation; - contextmenuList = this.props.showShareBtn ? [SHARE] : []; + + if (showShareBtn) { + contextmenuList = [SHARE]; + } if (dirent.permission === 'rw' || dirent.permission === 'r') { contextmenuList = [...contextmenuList, DOWNLOAD]; @@ -483,22 +489,22 @@ class DirentListView extends React.Component { if (dirent.permission === 'rw') { contextmenuList = [...contextmenuList, DELETE]; } - - contextmenuList = [...contextmenuList, 'Divider']; + contextmenuList = contextmenuList.length > 0 ? [...contextmenuList, 'Divider'] : []; } let { RENAME, MOVE, COPY, PERMISSION, OPEN_VIA_CLIENT, LOCK, UNLOCK, COMMENT, HISTORY, ACCESS_LOG, TAGS } = TextTranslation; if (type === 'dir' && permission === 'rw') { if (can_set_folder_perm) { - menuList = [...menuList, RENAME, MOVE, COPY, 'Divider', PERMISSION, 'Divider', OPEN_VIA_CLIENT]; + menuList = [...contextmenuList, RENAME, MOVE, COPY, 'Divider', PERMISSION, 'Divider', OPEN_VIA_CLIENT]; } else { - menuList = [...menuList, RENAME, MOVE, COPY, 'Divider', OPEN_VIA_CLIENT]; + menuList = [...contextmenuList, RENAME, MOVE, COPY, 'Divider', OPEN_VIA_CLIENT]; } return menuList; } if (type === 'dir' && permission === 'r') { - menuList = currentRepoInfo.encrypted ? [...contextmenuList, COPY] : []; + menuList = [...contextmenuList]; + menuList = currentRepoInfo.encrypted ? [...menuList, COPY] : menuList.slice(0, menuList.length - 1); return menuList; } @@ -551,7 +557,8 @@ class DirentListView extends React.Component { if (Utils.isIEBrower()) { return false; } - if (e.target.className === 'table-container ') { + this.enteredCounter++; + if (this.enteredCounter !== 0) { this.setState({isListDropTipShow: true}); } } @@ -568,7 +575,8 @@ class DirentListView extends React.Component { if (Utils.isIEBrower()) { return false; } - if (e.target.className === 'table-container table-drop-active') { + this.enteredCounter--; + if (this.enteredCounter === 0) { this.setState({isListDropTipShow: false}); } } @@ -578,6 +586,7 @@ class DirentListView extends React.Component { return false; } e.persist(); + this.enteredCounter = 0; this.setState({isListDropTipShow: false}); if (e.dataTransfer.files.length) { // uploaded files return; @@ -587,22 +596,19 @@ class DirentListView extends React.Component { let {nodeDirent, nodeParentPath, nodeRootPath} = dragStartItemData; - if (e.target.className === 'table-container table-drop-active') { - - if (Array.isArray(dragStartItemData)) { //selected items - return; - } - - if (nodeRootPath === this.props.path || nodeParentPath === this.props.path) { - return; - } - - if (this.props.path.indexOf(nodeRootPath) !== -1) { - return; - } - - this.props.onItemMove(this.props.currentRepoInfo, nodeDirent, this.props.path, nodeParentPath); + if (Array.isArray(dragStartItemData)) { //selected items + return; } + + if (nodeRootPath === this.props.path || nodeParentPath === this.props.path) { + return; + } + + if (this.props.path.indexOf(nodeRootPath) !== -1) { + return; + } + + this.props.onItemMove(this.props.currentRepoInfo, nodeDirent, this.props.path, nodeParentPath); } render() { @@ -659,7 +665,6 @@ class DirentListView extends React.Component { repoEncrypted={this.repoEncrypted} enableDirPrivateShare={this.props.enableDirPrivateShare} isGroupOwnedRepo={this.props.isGroupOwnedRepo} - showShareBtn={this.props.showShareBtn} onItemClick={this.props.onItemClick} onItemRenameToggle={this.onItemRenameToggle} onItemSelected={this.onItemSelected} diff --git a/frontend/src/components/toolbar/mutilple-dir-operation-toolbar.js b/frontend/src/components/toolbar/mutilple-dir-operation-toolbar.js index adbabd7d7d..7a2f93b442 100644 --- a/frontend/src/components/toolbar/mutilple-dir-operation-toolbar.js +++ b/frontend/src/components/toolbar/mutilple-dir-operation-toolbar.js @@ -32,7 +32,6 @@ const propTypes = { onFilesTagChanged: PropTypes.func.isRequired, unSelectDirent: PropTypes.func.isRequired, updateDirent: PropTypes.func.isRequired, - showShareBtn: PropTypes.bool.isRequired, }; class MutipleDirOperationToolbar extends React.Component { @@ -90,18 +89,19 @@ class MutipleDirOperationToolbar extends React.Component { getDirentMenuList = (dirent) => { let menuList = []; let currentRepoInfo = this.props.currentRepoInfo; + let showShareBtn = Utils.isHasPermissionToShare(currentRepoInfo, dirent.permission, dirent); const { SHARE, TAGS, RELATED_FILES, HISTORY, ACCESS_LOG, OPEN_VIA_CLIENT, LOCK, UNLOCK } = TextTranslation; - if (dirent.type === 'dir') { - let shareBtn = this.props.showShareBtn ? [SHARE] : []; - menuList = [...shareBtn]; + if (showShareBtn) { + menuList = [SHARE]; + } return menuList; } if (dirent.type === 'file') { - let shareBtn = (this.props.showShareBtn && canGenerateShareLink) ? [SHARE] : []; + let shareBtn = showShareBtn ? [SHARE] : []; menuList = [...shareBtn, TAGS, RELATED_FILES, 'Divider', HISTORY, ACCESS_LOG, 'Divider', OPEN_VIA_CLIENT]; if (!Utils.isMarkdownFile(dirent.name)) { diff --git a/frontend/src/components/tree-view/tree-view.js b/frontend/src/components/tree-view/tree-view.js index fb04d36907..e6193e9f42 100644 --- a/frontend/src/components/tree-view/tree-view.js +++ b/frontend/src/components/tree-view/tree-view.js @@ -198,6 +198,12 @@ class TreeView extends React.Component { } onContextMenu = (event) => { + event.preventDefault(); + + let currentRepoInfo = this.props.currentRepoInfo; + if (currentRepoInfo.permission !== 'admin' && currentRepoInfo.permission !== 'rw') { + return ''; + } this.handleContextClick(event); } @@ -208,12 +214,6 @@ class TreeView extends React.Component { if (!this.props.isNodeMenuShow) { return; } - - let currentRepoInfo = this.props.currentRepoInfo; - - if (currentRepoInfo.permission === 'cloud-edit' || currentRepoInfo.permission === 'preview') { - return ''; - } let x = event.clientX || (event.touches && event.touches[0].pageX); let y = event.clientY || (event.touches && event.touches[0].pageY); diff --git a/frontend/src/pages/lib-content-view/lib-content-container.js b/frontend/src/pages/lib-content-view/lib-content-container.js index a3e61883a4..f7f929735f 100644 --- a/frontend/src/pages/lib-content-view/lib-content-container.js +++ b/frontend/src/pages/lib-content-view/lib-content-container.js @@ -20,8 +20,8 @@ const propTypes = { repoID: PropTypes.string.isRequired, repoPermission: PropTypes.bool.isRequired, enableDirPrivateShare: PropTypes.bool.isRequired, - userPrem: PropTypes.bool, isGroupOwnedRepo: PropTypes.bool.isRequired, + userPerm: PropTypes.string, // path func onTabNavClick: PropTypes.func.isRequired, onMainNavBarClick: PropTypes.func.isRequired, @@ -58,7 +58,6 @@ const propTypes = { // list isDirentListLoading: PropTypes.bool.isRequired, direntList: PropTypes.array.isRequired, - showShareBtn: PropTypes.bool.isRequired, sortBy: PropTypes.string.isRequired, sortOrder: PropTypes.string.isRequired, sortItems: PropTypes.func.isRequired, @@ -160,7 +159,7 @@ class LibContentContainer extends React.Component { if (usedRepoTags.length !== 0 || readmeMarkdown !== null || draftCounts !== 0) { isRepoInfoBarShow = true; } - } + } return ( @@ -195,6 +194,7 @@ class LibContentContainer extends React.Component { repoID={repoID} currentRepoInfo={this.props.currentRepoInfo} isGroupOwnedRepo={this.props.isGroupOwnedRepo} + userPerm={this.props.userPerm} enableDirPrivateShare={this.props.enableDirPrivateShare} isRepoInfoBarShow={isRepoInfoBarShow} usedRepoTags={this.props.usedRepoTags} @@ -203,7 +203,6 @@ class LibContentContainer extends React.Component { updateUsedRepoTags={this.props.updateUsedRepoTags} isDirentListLoading={this.props.isDirentListLoading} direntList={this.props.direntList} - showShareBtn={this.props.showShareBtn} sortBy={this.props.sortBy} sortOrder={this.props.sortOrder} sortItems={this.props.sortItems} @@ -234,6 +233,7 @@ class LibContentContainer extends React.Component { currentRepoInfo={this.props.currentRepoInfo} repoPermission={this.props.repoPermission} isGroupOwnedRepo={this.props.isGroupOwnedRepo} + userPerm={this.props.userPerm} enableDirPrivateShare={this.props.enableDirPrivateShare} onRenameNode={this.props.onRenameNode} isRepoInfoBarShow={isRepoInfoBarShow} @@ -250,7 +250,6 @@ class LibContentContainer extends React.Component { onItemCopy={this.props.onItemCopy} updateDirent={this.props.updateDirent} onAddFolder={this.props.onAddFolder} - showShareBtn={this.props.showShareBtn} showDirentDetail={this.props.showDirentDetail} onGridItemClick={this.onGridItemClick} isDirentDetailShow={this.props.isDirentDetailShow} @@ -265,6 +264,7 @@ class LibContentContainer extends React.Component { currentRepoInfo={this.props.currentRepoInfo} repoPermission={this.props.repoPermission} isGroupOwnedRepo={this.props.isGroupOwnedRepo} + userPerm={this.props.userPerm} enableDirPrivateShare={this.props.enableDirPrivateShare} isTreeDataLoading={this.props.isTreeDataLoading} treeData={this.props.treeData} @@ -295,7 +295,6 @@ class LibContentContainer extends React.Component { updateUsedRepoTags={this.props.updateUsedRepoTags} isDirentListLoading={this.props.isDirentListLoading} direntList={this.props.direntList} - showShareBtn={this.props.showShareBtn} sortBy={this.props.sortBy} sortOrder={this.props.sortOrder} sortItems={this.props.sortItems} diff --git a/frontend/src/pages/lib-content-view/lib-content-view.js b/frontend/src/pages/lib-content-view/lib-content-view.js index a02647890e..19e7e9256d 100644 --- a/frontend/src/pages/lib-content-view/lib-content-view.js +++ b/frontend/src/pages/lib-content-view/lib-content-view.js @@ -1480,33 +1480,11 @@ class LibContentView extends React.Component { return ''; } - let showShareBtn = false; let enableDirPrivateShare = false; - let { currentRepoInfo, repoEncrypted, userPerm } = this.state; - let isAdmin = currentRepoInfo.is_admin; - let isVirtual = currentRepoInfo.is_virtual; + let { currentRepoInfo, userPerm } = this.state; + let showShareBtn = Utils.isHasPermissionToShare(currentRepoInfo, userPerm); let isRepoOwner = currentRepoInfo.owner_email === username; - if (!repoEncrypted) { - let showGenerateShareLinkTab = false; - if (canGenerateShareLink && (userPerm == 'rw' || userPerm == 'r')) { - showGenerateShareLinkTab = true; - } - let showGenerateUploadLinkTab = false; - if (canGenerateUploadLink && (userPerm == 'rw')) { - showGenerateUploadLinkTab = true; - } - - if (!isVirtual && (isRepoOwner || isAdmin)) { - enableDirPrivateShare = true; - } - - if (showGenerateShareLinkTab || showGenerateUploadLinkTab || enableDirPrivateShare) { - showShareBtn = true; - } - - } - let direntItemsList = this.state.direntList.filter((item, index) => { return index < this.state.itemsShowLength; }); @@ -1599,7 +1577,6 @@ class LibContentView extends React.Component { updateUsedRepoTags={this.updateUsedRepoTags} isDirentListLoading={this.state.isDirentListLoading} direntList={direntItemsList} - showShareBtn={showShareBtn} sortBy={this.state.sortBy} sortOrder={this.state.sortOrder} sortItems={this.sortItems} diff --git a/frontend/src/utils/utils.js b/frontend/src/utils/utils.js index e1bc531b7c..22ae858561 100644 --- a/frontend/src/utils/utils.js +++ b/frontend/src/utils/utils.js @@ -1,4 +1,4 @@ -import { mediaUrl, gettext, serviceURL, siteRoot } from './constants'; +import { mediaUrl, gettext, serviceURL, siteRoot, canGenerateShareLink, canGenerateUploadLink, username } from './constants'; import { strChineseFirstPY } from './pinyin-by-unicode'; export const Utils = { @@ -901,6 +901,56 @@ export const Utils = { } return event.target.getAttribute('data-' + data); + }, + + /** + * Check whether user has permission to share a dirent. + * If dirent is none, then check whether the user can share the repo + * scene 1: root path or folder path, control the share button in the toolbar + * scene 2: selected a dirent, control the share button in the toolbar dropdown-menu + * scene 3: dirent list(grid list), control the share button in the dirent-item or righe-menu + * + * @param {*} repoInfo + * @param {*} userDirPermission + * @param {*} dirent + */ + isHasPermissionToShare: function(repoInfo, userDirPermission, dirent) { + + let { is_admin: isAdmin, is_virtual: isVirtual, encrypted: repoEncrypted, owner_email: ownerEmail } = repoInfo; + let isRepoOwner = ownerEmail === username; + + if (repoEncrypted) { + return false; + } + + if (dirent && dirent.type === 'file') { + let hasGenerateShareLinkPermission = false; + if (canGenerateShareLink && (userDirPermission == 'rw' || userDirPermission == 'r')) { + hasGenerateShareLinkPermission = true; + } + return hasGenerateShareLinkPermission; + } + + // the root path or the dirent type is dir + let hasGenerateShareLinkPermission = false; + if (canGenerateShareLink && (userDirPermission == 'rw' || userDirPermission == 'r')) { + hasGenerateShareLinkPermission = true; + return hasGenerateShareLinkPermission; + } + + let hasGenerateUploadLinkPermission = false; + if (canGenerateUploadLink && (userDirPermission == 'rw')) { + hasGenerateUploadLinkPermission = true; + return hasGenerateUploadLinkPermission; + } + + let hasDirPrivateSharePermission = false; + if (!isVirtual && (isRepoOwner || isAdmin)) { + hasDirPrivateSharePermission = true; + return hasDirPrivateSharePermission; + } + + return false; } }; diff --git a/seahub/api2/endpoints/repos_batch.py b/seahub/api2/endpoints/repos_batch.py index dae4fed4c7..a17fbedd33 100644 --- a/seahub/api2/endpoints/repos_batch.py +++ b/seahub/api2/endpoints/repos_batch.py @@ -29,7 +29,7 @@ from seahub.utils import is_org_context, send_perm_audit_msg, \ normalize_dir_path, get_folder_permission_recursively, \ normalize_file_path, check_filename_with_rename from seahub.utils.repo import get_repo_owner, get_available_repo_perms, \ - parse_repo_perm, get_locked_files_by_dir + parse_repo_perm, get_locked_files_by_dir, get_sub_folder_permission_by_dir from seahub.views import check_folder_permission from seahub.settings import MAX_PATH @@ -1254,6 +1254,15 @@ class ReposAsyncBatchMoveItemView(APIView): error_msg = _(u'File %s is locked.') % dirent return api_error(status.HTTP_403_FORBIDDEN, error_msg) + # check sub folder permission + folder_permission_dict = get_sub_folder_permission_by_dir(request, + src_repo_id, src_parent_dir) + for dirent in src_dirents: + if dirent in folder_permission_dict.keys() and \ + folder_permission_dict[dirent] != 'rw': + error_msg = _(u"Can't move folder %s, please check its permission.") % dirent + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + # move file result = {} formated_src_dirents = [dirent.strip('/') for dirent in src_dirents] @@ -1464,6 +1473,15 @@ class ReposSyncBatchMoveItemView(APIView): error_msg = _(u'File %s is locked.') % dirent return api_error(status.HTTP_403_FORBIDDEN, error_msg) + # check sub folder permission + folder_permission_dict = get_sub_folder_permission_by_dir(request, + src_repo_id, src_parent_dir) + for dirent in src_dirents: + if dirent in folder_permission_dict.keys() and \ + folder_permission_dict[dirent] != 'rw': + error_msg = _(u"Can't move folder %s, please check its permission.") % dirent + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + # move file result = {} formated_src_dirents = [dirent.strip('/') for dirent in src_dirents] @@ -1483,3 +1501,87 @@ class ReposSyncBatchMoveItemView(APIView): result = {} result['success'] = True return Response(result) + + +class ReposBatchDeleteItemView(APIView): + + authentication_classes = (TokenAuthentication, SessionAuthentication) + permission_classes = (IsAuthenticated, ) + throttle_classes = (UserRateThrottle, ) + + def delete(self, request): + """ Multi delete files/folders. + Permission checking: + 1. User must has `rw` permission for parent folder. + Parameter: + { + "repo_id":"7460f7ac-a0ff-4585-8906-bb5a57d2e118", + "parent_dir":"/a/b/c/", + "dirents":["1.md", "2.md"], + } + """ + + # argument check + repo_id = request.data.get('repo_id', None) + if not repo_id: + error_msg = 'repo_id invalid.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + parent_dir = request.data.get('parent_dir', None) + if not parent_dir: + error_msg = 'parent_dir invalid.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + dirents = request.data.get('dirents', None) + if not dirents: + error_msg = 'dirents invalid.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + # resource check + if not seafile_api.get_repo(repo_id): + error_msg = 'Library %s not found.' % repo_id + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + if not seafile_api.get_dir_id_by_path(repo_id, parent_dir): + error_msg = 'Folder %s not found.' % parent_dir + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + # permission check + # User must has `rw` permission for parent dir. + if check_folder_permission(request, repo_id, parent_dir) != PERMISSION_READ_WRITE: + error_msg = 'Permission denied.' + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + + # check locked files + username = request.user.username + locked_files = get_locked_files_by_dir(request, repo_id, parent_dir) + for dirent in dirents: + # file is locked and lock owner is not current user + if dirent in locked_files.keys() and \ + locked_files[dirent] != username: + error_msg = _(u'File %s is locked.') % dirent + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + + # check sub folder permission + folder_permission_dict = get_sub_folder_permission_by_dir(request, repo_id, parent_dir) + for dirent in dirents: + if dirent in folder_permission_dict.keys() and \ + folder_permission_dict[dirent] != 'rw': + error_msg = _(u"Can't delete folder %s, please check its permission.") % dirent + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + + # delete file + result = {} + formated_dirents = [dirent.strip('/') for dirent in dirents] + multi_dirents_str = "\t".join(formated_dirents) + + try: + seafile_api.del_file(repo_id, parent_dir, multi_dirents_str, username) + except Exception as e: + logger.error(e) + error_msg = 'Internal Server Error' + return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) + + result = {} + result['success'] = True + return Response(result) diff --git a/seahub/urls.py b/seahub/urls.py index 7a2425d3e6..e68eff4d05 100644 --- a/seahub/urls.py +++ b/seahub/urls.py @@ -46,7 +46,8 @@ from seahub.api2.endpoints.repos_batch import ReposBatchView, \ ReposBatchCopyDirView, ReposBatchCreateDirView, \ ReposBatchCopyItemView, ReposBatchMoveItemView, \ ReposAsyncBatchCopyItemView, ReposAsyncBatchMoveItemView, \ - ReposSyncBatchCopyItemView, ReposSyncBatchMoveItemView + ReposSyncBatchCopyItemView, ReposSyncBatchMoveItemView, \ + ReposBatchDeleteItemView from seahub.api2.endpoints.repos import RepoView, ReposView from seahub.api2.endpoints.file import FileView from seahub.api2.endpoints.file_history import FileHistoryView, NewFileHistoryView @@ -317,11 +318,14 @@ urlpatterns = [ url(r'^api/v2.1/revision-tags/tag-names/$', TagNamesView.as_view(), name='api-v2.1-revision-tags-tag-names'), ## user::repos-batch-operate + # for icourt url(r'^api/v2.1/repos/batch/$', ReposBatchView.as_view(), name='api-v2.1-repos-batch'), url(r'^api/v2.1/repos/batch-copy-dir/$', ReposBatchCopyDirView.as_view(), name='api-v2.1-repos-batch-copy-dir'), url(r'^api/v2.1/repos/batch-create-dir/$', ReposBatchCreateDirView.as_view(), name='api-v2.1-repos-batch-create-dir'), url(r'^api/v2.1/repos/batch-copy-item/$', ReposBatchCopyItemView.as_view(), name='api-v2.1-repos-batch-copy-item'), url(r'^api/v2.1/repos/batch-move-item/$', ReposBatchMoveItemView.as_view(), name='api-v2.1-repos-batch-move-item'), + + url(r'^api/v2.1/repos/batch-delete-item/$', ReposBatchDeleteItemView.as_view(), name='api-v2.1-repos-batch-delete-item'), url(r'^api/v2.1/repos/async-batch-copy-item/$', ReposAsyncBatchCopyItemView.as_view(), name='api-v2.1-repos-async-batch-copy-item'), url(r'^api/v2.1/repos/async-batch-move-item/$', ReposAsyncBatchMoveItemView.as_view(), name='api-v2.1-repos-async-batch-move-item'), url(r'^api/v2.1/repos/sync-batch-copy-item/$', ReposSyncBatchCopyItemView.as_view(), name='api-v2.1-repos-sync-batch-copy-item'), diff --git a/seahub/utils/repo.py b/seahub/utils/repo.py index 161528c6a2..91520e7b3f 100644 --- a/seahub/utils/repo.py +++ b/seahub/utils/repo.py @@ -1,5 +1,6 @@ # Copyright (c) 2012-2016 Seafile Ltd. # -*- coding: utf-8 -*- +import stat import logging from collections import namedtuple @@ -208,6 +209,30 @@ def get_locked_files_by_dir(request, repo_id, folder_path): return locked_files +def get_sub_folder_permission_by_dir(request, repo_id, parent_dir): + """ Get sub folder permission in a folder + + Returns: + A dict contains folder name and permission. + + folder_permission_dict = { + 'folder_name_1': 'r'; + 'folder_name_2': 'rw'; + ... + } + """ + username = request.user.username + dir_id = seafile_api.get_dir_id_by_path(repo_id, parent_dir) + dirents = seafile_api.list_dir_with_perm(repo_id, + parent_dir, dir_id, username, -1, -1) + + folder_permission_dict = {} + for dirent in dirents: + if stat.S_ISDIR(dirent.mode): + folder_permission_dict[dirent.obj_name] = dirent.permission + + return folder_permission_dict + def get_shared_groups_by_repo(repo_id, org_id=None): if not org_id: group_ids = seafile_api.get_shared_group_ids_by_repo( diff --git a/tests/api/endpoints/test_repos_batch.py b/tests/api/endpoints/test_repos_batch.py index 10036efab2..f0232fc00c 100644 --- a/tests/api/endpoints/test_repos_batch.py +++ b/tests/api/endpoints/test_repos_batch.py @@ -1025,6 +1025,38 @@ class ReposAsyncBatchMoveItemView(BaseTestCase): json_resp = json.loads(resp.content) assert json_resp['error_msg'] == 'File %s is locked.' % admin_file_name + def test_move_with_r_permission_sub_folder(self): + + if not LOCAL_PRO_DEV_ENV: + return + + self.login_as(self.user) + + # share admin's tmp repo to user with 'rw' permission + admin_repo_id = self.create_new_repo(self.admin_name) + seafile_api.share_repo(admin_repo_id, self.admin_name, + self.user_name, 'rw') + + # admin set 'r' sub folder permission + admin_folder_name = randstring(6) + seafile_api.post_dir(admin_repo_id, '/', admin_folder_name, self.admin_name) + seafile_api.add_folder_user_perm(admin_repo_id, '/' + + admin_folder_name, 'r', self.user_name) + + # user move r permission folder + data = { + "src_repo_id": admin_repo_id, + "src_parent_dir": '/', + "src_dirents":[admin_folder_name], + "dst_repo_id": self.dst_repo_id, + "dst_parent_dir": '/', + } + resp = self.client.post(self.url, json.dumps(data), 'application/json') + self.assertEqual(403, resp.status_code) + json_resp = json.loads(resp.content) + assert json_resp['error_msg'] == "Can't move folder %s, please check its permission." % admin_folder_name + + class ReposSyncBatchCopyItemView(BaseTestCase): def create_new_repo(self, username): @@ -1577,7 +1609,7 @@ class ReposSyncBatchMoveItemView(BaseTestCase): self.login_as(self.user) - # share admin's tmp repo to user with 'r' permission + # share admin's tmp repo to user with 'rw' permission admin_repo_id = self.create_new_repo(self.admin_name) seafile_api.share_repo(admin_repo_id, self.admin_name, self.user_name, 'rw') @@ -1600,3 +1632,220 @@ class ReposSyncBatchMoveItemView(BaseTestCase): self.assertEqual(403, resp.status_code) json_resp = json.loads(resp.content) assert json_resp['error_msg'] == 'File %s is locked.' % admin_file_name + + def test_move_with_r_permission_sub_folder(self): + + if not LOCAL_PRO_DEV_ENV: + return + + self.login_as(self.user) + + # share admin's tmp repo to user with 'rw' permission + admin_repo_id = self.create_new_repo(self.admin_name) + seafile_api.share_repo(admin_repo_id, self.admin_name, + self.user_name, 'rw') + + # admin set 'r' sub folder permission + admin_folder_name = randstring(6) + seafile_api.post_dir(admin_repo_id, '/', admin_folder_name, self.admin_name) + seafile_api.add_folder_user_perm(admin_repo_id, '/' + + admin_folder_name, 'r', self.user_name) + + # user move r permission folder + data = { + "src_repo_id": admin_repo_id, + "src_parent_dir": '/', + "src_dirents":[admin_folder_name], + "dst_repo_id": self.dst_repo_id, + "dst_parent_dir": '/', + } + resp = self.client.post(self.url, json.dumps(data), 'application/json') + self.assertEqual(403, resp.status_code) + json_resp = json.loads(resp.content) + assert json_resp['error_msg'] == "Can't move folder %s, please check its permission." % admin_folder_name + +class ReposBatchDeleteItemView(BaseTestCase): + + def create_new_repo(self, username): + new_repo_id = seafile_api.create_repo(name=randstring(10), + desc='', username=username, passwd=None) + + return new_repo_id + + def setUp(self): + self.user_name = self.user.username + self.admin_name = self.admin.username + + self.repo_id = self.repo.id + + self.file_path = self.file + self.file_name = os.path.basename(self.file_path) + + self.folder_path = self.folder + self.folder_name = os.path.basename(self.folder) + + self.url = reverse('api-v2.1-repos-batch-delete-item') + + def tearDown(self): + self.remove_repo(self.repo_id) + + def test_can_delete(self): + + # items in parent folder + assert seafile_api.get_dir_id_by_path(self.repo_id, self.folder_path) != None + assert seafile_api.get_file_id_by_path(self.repo_id, self.file_path) != None + + self.login_as(self.user) + + data = { + "repo_id": self.repo_id, + "parent_dir": '/', + "dirents":[self.folder_name, self.file_name], + } + + resp = self.client.delete(self.url, json.dumps(data), + 'application/json') + self.assertEqual(200, resp.status_code) + + # items NOT in parent folder + assert seafile_api.get_dir_id_by_path(self.repo_id, self.folder_path) is None + assert seafile_api.get_file_id_by_path(self.repo_id, self.file_path) is None + + def test_delete_with_invalid_parameter(self): + + self.login_as(self.user) + + data = { + "parent_dir": '/', + "dirents":[self.folder_name, self.file_name], + } + resp = self.client.delete(self.url, json.dumps(data), 'application/json') + self.assertEqual(400, resp.status_code) + + data = { + "repo_id": self.repo_id, + "dirents":[self.folder_name, self.file_name], + } + resp = self.client.delete(self.url, json.dumps(data), 'application/json') + self.assertEqual(400, resp.status_code) + + data = { + "repo_id": self.repo_id, + "parent_dir": '/', + } + resp = self.client.delete(self.url, json.dumps(data), 'application/json') + self.assertEqual(400, resp.status_code) + + def test_delete_with_repo_not_exist(self): + + self.login_as(self.user) + + invalid_repo_id = 'd53fe97e-919a-42f9-a29f-042d285ba6fb' + data = { + "repo_id": invalid_repo_id, + "parent_dir": '/', + "dirents":[self.folder_name, self.file_name], + } + resp = self.client.delete(self.url, json.dumps(data), 'application/json') + self.assertEqual(404, resp.status_code) + + def test_delete_with_folder_not_exist(self): + + self.login_as(self.user) + + data = { + "repo_id": self.repo_id, + "parent_dir": 'invalid_folder', + "dirents":[self.folder_name, self.file_name], + } + resp = self.client.delete(self.url, json.dumps(data), 'application/json') + self.assertEqual(404, resp.status_code) + + def test_delete_with_invalid_repo_permission(self): + + tmp_repo_id = self.create_new_repo(self.admin_name) + + self.login_as(self.user) + + data = { + "repo_id": tmp_repo_id, + "parent_dir": '/', + "dirents":[self.folder_name, self.file_name], + } + resp = self.client.delete(self.url, json.dumps(data), 'application/json') + self.assertEqual(403, resp.status_code) + + def test_delete_with_invalid_parent_folder_permission(self): + + self.login_as(self.user) + + # share admin's tmp repo to user with 'r' permission + admin_repo_id = self.create_new_repo(self.admin_name) + seafile_api.share_repo(admin_repo_id, self.admin_name, + self.user_name, 'r') + data = { + "repo_id": admin_repo_id, + "parent_dir": '/', + "dirents":[self.folder_name, self.file_name], + } + resp = self.client.delete(self.url, json.dumps(data), 'application/json') + self.assertEqual(403, resp.status_code) + self.remove_repo(admin_repo_id) + + def test_delete_with_locked_file(self): + + if not LOCAL_PRO_DEV_ENV: + return + + self.login_as(self.user) + + # share admin's tmp repo to user with 'r' permission + admin_repo_id = self.create_new_repo(self.admin_name) + seafile_api.share_repo(admin_repo_id, self.admin_name, + self.user_name, 'rw') + + # admin lock file + admin_file_name = randstring(6) + seafile_api.post_empty_file(admin_repo_id, '/', admin_file_name, + self.admin_name) + seafile_api.lock_file(admin_repo_id, admin_file_name, self.admin_name, 0) + + # user move locked file + data = { + "repo_id": admin_repo_id, + "parent_dir": '/', + "dirents":[admin_file_name], + } + resp = self.client.delete(self.url, json.dumps(data), 'application/json') + self.assertEqual(403, resp.status_code) + json_resp = json.loads(resp.content) + assert json_resp['error_msg'] == 'File %s is locked.' % admin_file_name + + def test_delete_with_r_permission_sub_folder(self): + + if not LOCAL_PRO_DEV_ENV: + return + + self.login_as(self.user) + + # share admin's tmp repo to user with 'r' permission + admin_repo_id = self.create_new_repo(self.admin_name) + seafile_api.share_repo(admin_repo_id, self.admin_name, + self.user_name, 'rw') + + # admin set 'r' sub folder permission + admin_folder_name = randstring(6) + seafile_api.post_dir(admin_repo_id, '/', admin_folder_name, self.admin_name) + seafile_api.add_folder_user_perm(admin_repo_id, '/' + + admin_folder_name, 'r', self.user_name) + + # user move locked file + data = { + "repo_id": admin_repo_id, + "parent_dir": '/', + "dirents":[admin_folder_name], + } + resp = self.client.delete(self.url, json.dumps(data), 'application/json') + self.assertEqual(403, resp.status_code) + json_resp = json.loads(resp.content) + assert json_resp['error_msg'] == "Can't delete folder %s, please check its permission." % admin_folder_name