diff --git a/frontend/config/webpack.config.dev.js b/frontend/config/webpack.config.dev.js index 9f1d21e9d2..0d47dcc1f3 100644 --- a/frontend/config/webpack.config.dev.js +++ b/frontend/config/webpack.config.dev.js @@ -204,6 +204,11 @@ module.exports = { require.resolve('react-dev-utils/webpackHotDevClient'), paths.appSrc + "/repo-history.js", ], + repoSnapshot: [ + require.resolve('./polyfills'), + require.resolve('react-dev-utils/webpackHotDevClient'), + paths.appSrc + "/repo-snapshot.js", + ], repoFolderTrash: [ require.resolve('./polyfills'), require.resolve('react-dev-utils/webpackHotDevClient'), diff --git a/frontend/config/webpack.config.prod.js b/frontend/config/webpack.config.prod.js index 3468f4f578..c56d323b3e 100644 --- a/frontend/config/webpack.config.prod.js +++ b/frontend/config/webpack.config.prod.js @@ -89,6 +89,7 @@ module.exports = { viewFileUnknown: [require.resolve('./polyfills'), paths.appSrc + "/view-file-unknown.js"], settings: [require.resolve('./polyfills'), paths.appSrc + "/settings.js"], repoHistory: [require.resolve('./polyfills'), paths.appSrc + "/repo-history.js"], + repoSnapshot: [require.resolve('./polyfills'), paths.appSrc + "/repo-snapshot.js"], repoFolderTrash: [require.resolve('./polyfills'), paths.appSrc + "/repo-folder-trash.js"], orgAdmin: [require.resolve('./polyfills'), paths.appSrc + "/pages/org-admin"], sysAdmin: [require.resolve('./polyfills'), paths.appSrc + "/pages/sys-admin"], diff --git a/frontend/package-lock.json b/frontend/package-lock.json index fd012dfb4f..8754b71214 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -15921,9 +15921,9 @@ } }, "seafile-js": { - "version": "0.2.91", - "resolved": "https://registry.npmjs.org/seafile-js/-/seafile-js-0.2.91.tgz", - "integrity": "sha512-QNm629wX+NmCUzKiqOh1h/LWRwnRvwKwRXU6qOdOHg2CyubgGXmzk6U9NPMW1oK3p0takfz/1xypPQxNIAsXSw==", + "version": "0.2.92", + "resolved": "https://registry.npmjs.org/seafile-js/-/seafile-js-0.2.92.tgz", + "integrity": "sha512-GySMa7cr8LXWj+uyIOG9JAIzUlzuGHZ1dLbx0tKrUcJ9vY10xVnDPCmJz3jEnG59wXNSlqkwcEaQxvaANfdgeg==", "requires": { "axios": "^0.18.0", "form-data": "^2.3.2", diff --git a/frontend/package.json b/frontend/package.json index 73ec5e61fc..89e5fe4272 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -37,7 +37,7 @@ "react-responsive": "^6.1.1", "react-select": "^2.4.1", "reactstrap": "^6.4.0", - "seafile-js": "^0.2.91", + "seafile-js": "^0.2.92", "socket.io-client": "^2.2.0", "sw-precache-webpack-plugin": "0.11.4", "unified": "^7.0.0", diff --git a/frontend/src/components/dialog/confirm-restore-repo.js b/frontend/src/components/dialog/confirm-restore-repo.js new file mode 100644 index 0000000000..e20fe08da7 --- /dev/null +++ b/frontend/src/components/dialog/confirm-restore-repo.js @@ -0,0 +1,46 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { Modal, ModalHeader, ModalBody, ModalFooter, Button } from 'reactstrap'; +import { gettext } from '../../utils/constants'; + +const propTypes = { + restoreRepo: PropTypes.func.isRequired, + toggle: PropTypes.func.isRequired +}; + +class ConfirmRestoreRepo extends Component { + + constructor(props) { + super(props); + this.state = { + btnDisabled: false + }; + } + + action = () => { + this.setState({ + btnDisabled: true + }); + this.props.restoreRepo(); + } + + render() { + const {formActionURL, csrfToken, toggle} = this.props; + return ( + + {gettext('Restore Library')} + +

{gettext('Are you sure you want to restore this library?')}

+
+ + + + +
+ ); + } +} + +ConfirmRestoreRepo.propTypes = propTypes; + +export default ConfirmRestoreRepo; diff --git a/frontend/src/components/dir-view-mode/dir-column-nav.js b/frontend/src/components/dir-view-mode/dir-column-nav.js index 845b4df570..d952464176 100644 --- a/frontend/src/components/dir-view-mode/dir-column-nav.js +++ b/frontend/src/components/dir-view-mode/dir-column-nav.js @@ -33,6 +33,7 @@ const propTypes = { onItemMove: PropTypes.func.isRequired, onItemCopy: PropTypes.func.isRequired, selectedDirentList: PropTypes.array.isRequired, + onItemsMove: PropTypes.func.isRequired, }; class DirColumnNav extends React.Component { @@ -276,6 +277,7 @@ class DirColumnNav extends React.Component { onItemMove={this.props.onItemMove} currentRepoInfo={this.props.currentRepoInfo} selectedDirentList={this.props.selectedDirentList} + onItemsMove={this.props.onItemsMove} />) } 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 12ef40d4ef..65fa1c7017 100644 --- a/frontend/src/components/dir-view-mode/dir-column-view.js +++ b/frontend/src/components/dir-view-mode/dir-column-view.js @@ -171,6 +171,7 @@ class DirColumnView extends React.Component { onItemMove={this.props.onItemMove} onItemCopy={this.props.onItemCopy} selectedDirentList={this.props.selectedDirentList} + onItemsMove={this.props.onItemsMove} />
diff --git a/frontend/src/components/dirent-detail/dirent-details.js b/frontend/src/components/dirent-detail/dirent-details.js index a2d9334d13..b4a27e0833 100644 --- a/frontend/src/components/dirent-detail/dirent-details.js +++ b/frontend/src/components/dirent-detail/dirent-details.js @@ -189,7 +189,7 @@ class DirentDetail extends React.Component { } render() { - let { dirent } = this.props; + let { dirent, repoID, path } = this.props; let { folderDirent } = this.state; if (!dirent && !folderDirent) { return ''; @@ -198,7 +198,7 @@ class DirentDetail extends React.Component { let bigIconUrl = dirent ? Utils.getDirentIcon(dirent, true) : Utils.getDirentIcon(folderDirent, true); const isImg = dirent ? Utils.imageCheck(dirent.name) : Utils.imageCheck(folderDirent.name); if (isImg) { - bigIconUrl = siteRoot + 'thumbnail/' + this.props.repoID + '/1024/' + dirent.name; + bigIconUrl = `${siteRoot}thumbnail/${repoID}/1024` + Utils.encodePath(`${path === '/' ? '' : path}/${dirent.name}`); } let direntName = dirent ? dirent.name : folderDirent.name; 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 59338ef9d8..81822b790f 100644 --- a/frontend/src/components/dirent-list-view/dirent-list-item.js +++ b/frontend/src/components/dirent-list-view/dirent-list-item.js @@ -49,6 +49,7 @@ const propTypes = { onFileTagChanged: PropTypes.func, enableDirPrivateShare: PropTypes.bool.isRequired, showDirentDetail: PropTypes.func.isRequired, + onItemsMove: PropTypes.func.isRequired, }; class DirentListItem extends React.Component { @@ -349,15 +350,28 @@ class DirentListItem extends React.Component { if (Utils.isIEBrower()) { return false; } - let nodeRootPath = ''; - nodeRootPath = this.props.path === '/' ? `${this.props.path}${this.props.dirent.name}` : `${this.props.path}/${this.props.dirent.name}`; - let dragStartItemData = {nodeDirent: this.props.dirent, nodeParentPath: this.props.path, nodeRootPath: nodeRootPath}; - dragStartItemData = JSON.stringify(dragStartItemData); e.dataTransfer.effectAllowed = 'move'; if (e.dataTransfer && e.dataTransfer.setDragImage) { e.dataTransfer.setDragImage(this.refs.drag_icon, 15, 15); } + + let { selectedDirentList } = this.props; + if (selectedDirentList.length > 0 && selectedDirentList.includes(this.props.dirent)) { // drag items and selectedDirentList include item + let selectedList = selectedDirentList.map(item => { + let nodeRootPath = this.getDirentPath(item); + let dragStartItemData = {nodeDirent: item, nodeParentPath: this.props.path, nodeRootPath: nodeRootPath}; + return dragStartItemData; + }); + selectedList = JSON.stringify(selectedList); + e.dataTransfer.setData('applicaiton/drag-item-info', selectedList); + return ; + } + + let nodeRootPath = this.getDirentPath(this.props.dirent); + let dragStartItemData = {nodeDirent: this.props.dirent, nodeParentPath: this.props.path, nodeRootPath: nodeRootPath}; + dragStartItemData = JSON.stringify(dragStartItemData); + e.dataTransfer.setData('applicaiton/drag-item-info', dragStartItemData); } @@ -395,7 +409,22 @@ class DirentListItem extends React.Component { } let dragStartItemData = e.dataTransfer.getData('applicaiton/drag-item-info'); dragStartItemData = JSON.parse(dragStartItemData); - let {nodeDirent, nodeParentPath, nodeRootPath} = dragStartItemData; + if (Array.isArray(dragStartItemData)) { //move items + let direntPaths = dragStartItemData.map(draggedItem => { + return draggedItem.nodeRootPath + }); + + let selectedPath = Utils.joinPath(this.props.path, this.props.dirent.name); + + if (direntPaths.some(direntPath => { return direntPath === selectedPath;})) { //eg; A/B, A/C --> A/B + return; + } + + this.props.onItemsMove(this.props.currentRepoInfo, selectedPath); + return ; + } + + let { nodeDirent, nodeParentPath, nodeRootPath } = dragStartItemData; let dropItemData = this.props.dirent; if (nodeDirent.name === dropItemData.name) { 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 a032e38998..84d2107539 100644 --- a/frontend/src/components/dirent-list-view/dirent-list-view.js +++ b/frontend/src/components/dirent-list-view/dirent-list-view.js @@ -577,6 +577,11 @@ 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; } @@ -664,6 +669,7 @@ class DirentListView extends React.Component { onFileTagChanged={this.props.onFileTagChanged} getDirentItemMenuList={this.getDirentItemMenuList} showDirentDetail={this.props.showDirentDetail} + onItemsMove={this.props.onItemsMove} /> ); })} diff --git a/frontend/src/components/file-uploader/file-uploader.js b/frontend/src/components/file-uploader/file-uploader.js index 26d4b21eee..da30a6ad22 100644 --- a/frontend/src/components/file-uploader/file-uploader.js +++ b/frontend/src/components/file-uploader/file-uploader.js @@ -417,8 +417,13 @@ class FileUploader extends React.Component { let repoID = this.props.repoID; seafileAPI.getUploadLink(repoID, this.props.path).then(res => { this.resumable.opts.target = res.data; - this.uploadInput.current.click(); + if (Utils.isIEBrower()) { + this.uploadInput.current.click(); + } }); + if (!Utils.isIEBrower()) { + this.uploadInput.current.click(); + } } onFolderUpload = () => { @@ -426,8 +431,13 @@ class FileUploader extends React.Component { let repoID = this.props.repoID; seafileAPI.getUploadLink(repoID, this.props.path).then(res => { this.resumable.opts.target = res.data; - this.uploadInput.current.click(); + if (Utils.isIEBrower()) { + this.uploadInput.current.click(); + } }); + if (!Utils.isIEBrower()) { + this.uploadInput.current.click(); + } } onDragStart = () => { diff --git a/frontend/src/components/tree-view/tree-view.js b/frontend/src/components/tree-view/tree-view.js index c3560ed5bc..fb04d36907 100644 --- a/frontend/src/components/tree-view/tree-view.js +++ b/frontend/src/components/tree-view/tree-view.js @@ -18,6 +18,7 @@ const propTypes = { onItemMove: PropTypes.func, currentRepoInfo: PropTypes.object, selectedDirentList: PropTypes.array, + onItemsMove: PropTypes.func, }; const PADDING_LEFT = 20; @@ -91,6 +92,20 @@ class TreeView extends React.Component { let {nodeDirent, nodeParentPath, nodeRootPath} = dragStartNodeData; let dropNodeData = node; + if (Array.isArray(dragStartNodeData)) { //move items + if (!dropNodeData) { //move items to root + if (dragStartNodeData[0].nodeParentPath === '/') { + this.setState({isTreeViewDropTipShow: false}); + return; + } + this.props.onItemsMove(this.props.currentRepoInfo, '/'); + this.setState({isTreeViewDropTipShow: false}); + return; + } + this.onMoveItems(dragStartNodeData, dropNodeData, this.props.currentRepoInfo, dropNodeData.path); + return; + } + if (!dropNodeData) { if (nodeParentPath === '/') { this.setState({isTreeViewDropTipShow: false}); @@ -119,7 +134,8 @@ class TreeView extends React.Component { // copy the dirent to it's child. eg: A/B -> A/B/C if (dropNodeData.object.type === 'dir' && nodeDirent.type === 'dir') { if (dropNodeData.parentNode.path !== nodeParentPath) { - if (dropNodeData.path.indexOf(nodeRootPath) !== -1) { + let paths = Utils.getPaths(dropNodeData.path); + if (paths.includes(nodeRootPath)) { return; } } @@ -128,6 +144,39 @@ class TreeView extends React.Component { this.onItemMove(this.props.currentRepoInfo, nodeDirent, dropNodeData.path, nodeParentPath); } + onMoveItems = (dragStartNodeData, dropNodeData, destRepo, destDirentPath) => { + let direntPaths = []; + let paths = Utils.getPaths(destDirentPath); + dragStartNodeData.forEach(dirent => { + let path = dirent.nodeRootPath; + direntPaths.push(path); + }); + + if (dropNodeData.object.type !== 'dir') { + return; + } + + // move dirents to one of them. eg: A/B, A/C -> A/B + if (direntPaths.some(direntPath => { return direntPath === destDirentPath;})) { + return; + } + + // move dirents to current path + if (dragStartNodeData[0].nodeParentPath && dragStartNodeData[0].nodeParentPath === dropNodeData.path ) { + return; + } + + // move dirents to one of their child. eg: A/B, A/D -> A/B/C + let isChildPath = direntPaths.some(direntPath => { + return paths.includes(direntPath); + }); + if (isChildPath) { + return; + } + + this.props.onItemsMove(destRepo, destDirentPath); + } + freezeItem = () => { this.setState({isItemFreezed: true}); } diff --git a/frontend/src/css/repo-snapshot.css b/frontend/src/css/repo-snapshot.css new file mode 100644 index 0000000000..cf63ab3952 --- /dev/null +++ b/frontend/src/css/repo-snapshot.css @@ -0,0 +1,37 @@ +body { + overflow: hidden; +} +#wrapper { + height: 100%; +} +.top-header { + background: #f4f4f7; + border-bottom: 1px solid #e8e8e8; + padding: .5rem 1rem; + flex-shrink: 0; +} +.go-back { + color: #c0c0c0; + font-size: 1.75rem; + position: absolute; + left: -40px; + top: -5px; +} +.op-bar { + padding: 9px 10px; + background: #f2f2f2; + border-radius: 2px; +} +.op-bar-btn { + border-color: #ccc; + border-radius: 2px; + height: 30px; + line-height: 28px; + font-weight: normal; + padding: 0 0.5rem; + min-width: 55px; +} +.heading-commit-time { + font-weight: normal; + font-size: 60%; +} diff --git a/frontend/src/repo-history.js b/frontend/src/repo-history.js index 697f9e8a47..f391653748 100644 --- a/frontend/src/repo-history.js +++ b/frontend/src/repo-history.js @@ -294,7 +294,7 @@ class Item extends React.Component { {userPerm == 'rw' && ( item.isFirstCommit ? {gettext('Current Version')} : - {gettext('View Snapshot')} + {gettext('View Snapshot')} )} diff --git a/frontend/src/repo-snapshot.js b/frontend/src/repo-snapshot.js new file mode 100644 index 0000000000..b0750bfa55 --- /dev/null +++ b/frontend/src/repo-snapshot.js @@ -0,0 +1,337 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import { navigate } from '@reach/router'; +import moment from 'moment'; +import { Utils } from './utils/utils'; +import { gettext, loginUrl, siteRoot, mediaUrl, logoPath, logoWidth, logoHeight, siteTitle } from './utils/constants'; +import { seafileAPI } from './utils/seafile-api'; +import Loading from './components/loading'; +import ModalPortal from './components/modal-portal'; +import toaster from './components/toast'; +import CommonToolbar from './components/toolbar/common-toolbar'; +import ConfirmRestoreRepo from './components/dialog/confirm-restore-repo'; + +import './css/toolbar.css'; +import './css/search.css'; + +import './css/repo-snapshot.css'; + +const { + repoID, repoName, isRepoOwner, + commitID, commitTime, commitDesc, commitRelativeTime, + showAuthor, authorAvatarURL, authorName, authorNickName +} = window.app.pageOptions; + +class RepoSnapshot extends React.Component { + + constructor(props) { + super(props); + this.state = { + isLoading: true, + errorMsg: '', + folderPath: '/', + folderItems: [], + isConfirmDialogOpen: false + }; + } + + componentDidMount() { + this.renderFolder(this.state.folderPath); + } + + toggleDialog = () => { + this.setState({ + isConfirmDialogOpen: !this.state.isConfirmDialogOpen + }); + } + + onSearchedClick = (selectedItem) => { + if (selectedItem.is_dir === true) { + let url = siteRoot + 'library/' + selectedItem.repo_id + '/' + selectedItem.repo_name + selectedItem.path; + navigate(url, {repalce: true}); + } else { + let url = siteRoot + 'lib/' + selectedItem.repo_id + '/file' + Utils.encodePath(selectedItem.path); + let newWindow = window.open('about:blank'); + newWindow.location.href = url; + } + } + + goBack = (e) => { + e.preventDefault(); + window.history.back(); + } + + renderFolder = (folderPath) => { + this.setState({ + folderPath: folderPath, + folderItems: [], + isLoading: true + }); + + seafileAPI.listCommitDir(repoID, commitID, folderPath).then((res) => { + this.setState({ + isLoading: false, + folderItems: res.data.dirent_list + }); + }).catch((error) => { + if (error.response) { + if (error.response.status == 403) { + this.setState({ + isLoading: false, + errorMsg: gettext('Permission denied') + }); + location.href = `${loginUrl}?next=${encodeURIComponent(location.href)}`; + } else { + this.setState({ + isLoading: false, + errorMsg: gettext('Error') + }); + } + } else { + this.setState({ + isLoading: false, + errorMsg: gettext('Please check the network.') + }); + } + }); + } + + clickFolderPath = (folderPath, e) => { + e.preventDefault(); + this.renderFolder(folderPath); + } + + renderPath = () => { + const path = this.state.folderPath; + const pathList = path.split('/'); + + if (path == '/') { + return repoName; + } + + return ( + + {repoName} + / + {pathList.map((item, index) => { + if (index > 0 && index != pathList.length - 1) { + return ( + + {pathList[index]} + / + + ); + } + } + )} + {pathList[pathList.length - 1]} + + ); + } + + restoreRepo = () => { + seafileAPI.revertRepo(repoID, commitID).then((res) => { + this.toggleDialog(); + toaster.success(gettext('Successfully restored the library.')); + }).catch((error) => { + let errorMsg = ''; + if (error.response) { + if (error.response.data && error.response.data['error_msg']) { + errorMsg = error.response.data['error_msg']; + } else { + errorMsg = gettext('Error'); + } + } else { + errorMsg = gettext('Please check the network.'); + } + this.toggleDialog(); + toaster.danger(errorMsg); + }); + } + + render() { + const { isConfirmDialogOpen, folderPath } = this.state; + + return ( + +
+
+ + logo + + +
+
+
+
+

(${commitTime})`}}>

+ + + + {folderPath == '/' && ( +
+

{commitDesc}

+
+ {showAuthor ? ( + + + {authorNickName} + + ) : {gettext('Unknown')}} +

+
+
+ )} +
+

{gettext('Current path: ')}{this.renderPath()}

+ {(folderPath == '/' && isRepoOwner) && + + } +
+ +
+
+
+
+ {isConfirmDialogOpen && + + + + } +
+ ); + } +} + +class Content extends React.Component { + + constructor(props) { + super(props); + this.theadData = [ + {width: '5%', text: ''}, + {width: '55%', text: gettext('Name')}, + {width: '20%', text: gettext('Size')}, + {width: '20%', text: ''} + ]; + } + + render() { + const { isLoading, errorMsg, folderPath, folderItems } = this.props.data; + + if (isLoading) { + return ; + } + + if (errorMsg) { + return

{errorMsg}

; + } + + return ( + + + + {this.theadData.map((item, index) => { + return ; + })} + + + + {folderItems.map((item, index) => { + return ; + }) + } + +
{item.text}
+ ); + } +} + +class FolderItem extends React.Component { + + constructor(props) { + super(props); + this.state = { + isIconShown: false + }; + } + + handleMouseOver = () => { + this.setState({isIconShown: true}); + } + + handleMouseOut = () => { + this.setState({isIconShown: false}); + } + + restoreItem = (e) => { + e.preventDefault(); + + const item = this.props.item; + const path = Utils.joinPath(this.props.folderPath, item.name); + const request = item.type == 'dir' ? + seafileAPI.revertFolder(repoID, path, commitID): + seafileAPI.revertFile(repoID, path, commitID); + request.then((res) => { + toaster.success(gettext('Successfully restored 1 item.')); + }).catch((error) => { + let errorMsg = ''; + if (error.response) { + errorMsg = error.response.data.error_msg || gettext('Error'); + } else { + errorMsg = gettext('Please check the network.'); + } + toaster.danger(errorMsg); + }); + } + + renderFolder = (e) => { + e.preventDefault(); + + const item = this.props.item; + const { folderPath } = this.props; + this.props.renderFolder(Utils.joinPath(folderPath, item.name)); + } + + render() { + const item = this.props.item; + const { isIconShown } = this.state; + const { folderPath } = this.props; + + return item.type == 'dir' ? ( + + {gettext('Directory')} + {item.name} + + + + + + ) : ( + + {gettext('File')} + {item.name} + {Utils.bytesToSize(item.size)} + + + + + + ); + } +} + +ReactDOM.render( + , + document.getElementById('wrapper') +); diff --git a/frontend/src/settings.js b/frontend/src/settings.js index 0f51415233..3d58039034 100644 --- a/frontend/src/settings.js +++ b/frontend/src/settings.js @@ -116,7 +116,7 @@ class Settings extends React.Component {
-
+
diff --git a/seahub/api2/endpoints/deleted_repos.py b/seahub/api2/endpoints/deleted_repos.py index 1010743dd4..95229d1338 100644 --- a/seahub/api2/endpoints/deleted_repos.py +++ b/seahub/api2/endpoints/deleted_repos.py @@ -6,8 +6,6 @@ from rest_framework.response import Response from rest_framework import status from seaserv import seafile_api -from pysearpc import SearpcError - from seahub.signals import repo_restored from seahub.api2.throttling import UserRateThrottle from seahub.api2.authentication import TokenAuthentication @@ -71,7 +69,7 @@ class DeletedRepos(APIView): try: seafile_api.restore_repo_from_trash(repo_id) repo_restored.send(sender=None, repo_id=repo_id, operator=username) - except SearpcError as e: + except Exception as e: logger.error(e) error_msg = "Internal Server Error" return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) diff --git a/seahub/api2/endpoints/repo_commit_dir.py b/seahub/api2/endpoints/repo_commit_dir.py index 645f3c550b..7498a0b6af 100644 --- a/seahub/api2/endpoints/repo_commit_dir.py +++ b/seahub/api2/endpoints/repo_commit_dir.py @@ -61,7 +61,7 @@ class RepoCommitDirView(APIView): commit = seafile_api.get_commit(repo.id, repo.version, commit_id) if not commit: - error_msg = 'Commit %s not found.' % commit + error_msg = 'Commit %s not found.' % commit_id return api_error(status.HTTP_404_NOT_FOUND, error_msg) dir_id = seafile_api.get_dir_id_by_commit_and_path(repo_id, commit_id, path) diff --git a/seahub/api2/endpoints/repo_commit_revert.py b/seahub/api2/endpoints/repo_commit_revert.py new file mode 100644 index 0000000000..31a061be26 --- /dev/null +++ b/seahub/api2/endpoints/repo_commit_revert.py @@ -0,0 +1,70 @@ +# Copyright (c) 2012-2019 Seafile Ltd. +# encoding: utf-8 + +import logging + +from django.utils.translation import ugettext as _ +from rest_framework.authentication import SessionAuthentication +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response +from rest_framework.views import APIView +from rest_framework import status + +from seahub.api2.throttling import UserRateThrottle +from seahub.api2.authentication import TokenAuthentication +from seahub.api2.utils import api_error +from seahub.views import check_folder_permission +from seaserv import seafile_api +from seahub.utils.repo import is_repo_owner +from seahub.constants import PERMISSION_READ_WRITE + +logger = logging.getLogger(__name__) + + +class RepoCommitRevertView(APIView): + authentication_classes = (TokenAuthentication, SessionAuthentication) + permission_classes = (IsAuthenticated,) + throttle_classes = (UserRateThrottle,) + + def post(self, request, repo_id, commit_id, format=None): + """ revert commit in repo history + + Permission checking: + 1. only repo owner can perform this action. + """ + username = request.user.username + + # resource check + repo = seafile_api.get_repo(repo_id) + if not repo: + error_msg = 'Library %s not found.' % repo_id + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + commit = seafile_api.get_commit(repo.id, repo.version, commit_id) + if not commit: + error_msg = 'Commit %s not found.' % commit_id + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + # permission check + if not is_repo_owner(request, repo_id, username) or \ + check_folder_permission(request, repo_id, '/') != PERMISSION_READ_WRITE: + error_msg = 'Permission denied.' + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + + # main + if repo.encrypted: + ret = seafile_api.is_password_set(repo_id, username) + is_decrypted = False if ret == 0 else True + + if not is_decrypted: + error_msg = _('This library has not been decrypted.') + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + + try: + seafile_api.revert_repo(repo_id, commit_id, username) + except Exception as e: + logger.error(e) + error_msg = 'Internal Server Error' + return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) + + return Response({'success': True}) diff --git a/seahub/api2/endpoints/repos_batch.py b/seahub/api2/endpoints/repos_batch.py index 72b444b39b..dae4fed4c7 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 + parse_repo_perm, get_locked_files_by_dir from seahub.views import check_folder_permission from seahub.settings import MAX_PATH @@ -1244,9 +1244,18 @@ class ReposAsyncBatchMoveItemView(APIView): error_msg = 'Permission denied.' return api_error(status.HTTP_403_FORBIDDEN, error_msg) - result = {} + # check locked files username = request.user.username + locked_files = get_locked_files_by_dir(request, src_repo_id, src_parent_dir) + for dirent in src_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) + # move file + result = {} formated_src_dirents = [dirent.strip('/') for dirent in src_dirents] src_multi = "\t".join(formated_src_dirents) dst_multi = "\t".join(formated_src_dirents) @@ -1445,9 +1454,18 @@ class ReposSyncBatchMoveItemView(APIView): error_msg = 'Permission denied.' return api_error(status.HTTP_403_FORBIDDEN, error_msg) - result = {} + # check locked files username = request.user.username + locked_files = get_locked_files_by_dir(request, src_repo_id, src_parent_dir) + for dirent in src_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) + # move file + result = {} formated_src_dirents = [dirent.strip('/') for dirent in src_dirents] src_multi = "\t".join(formated_src_dirents) dst_multi = "\t".join(formated_src_dirents) diff --git a/seahub/handlers.py b/seahub/handlers.py index c327179ac1..6cca286d96 100644 --- a/seahub/handlers.py +++ b/seahub/handlers.py @@ -17,16 +17,17 @@ try: repo_id = kwargs['repo_id'] repo_name = kwargs['repo_name'] - # Move here to avoid model import during Django setup. - # TODO: Don't register signal/hanlders during Seahub start. + # TODO: Don't register signal/handlers during Seahub start. + if org_id > 0: related_users = seafile_api.org_get_shared_users_by_repo(org_id, repo_id) else: related_users = seafile_api.get_shared_users_by_repo(repo_id) org_id = -1 - related_users.append(creator) + if creator not in related_users: + related_users.append(creator) record = { 'op_type':'create', @@ -78,7 +79,8 @@ try: related_users = seafile_api.get_shared_users_by_repo(repo_id) org_id = -1 - related_users.append(repo_owner) + if repo_owner not in related_users: + related_users.append(repo_owner) record = { 'op_type':'delete', @@ -113,7 +115,9 @@ try: related_users = seafile_api.get_shared_users_by_repo(repo_id) org_id = -1 - related_users.append(repo_owner) + if repo_owner not in related_users: + related_users.append(repo_owner) + record = { 'op_type':'clean-up-trash', 'obj_type':'repo', @@ -144,7 +148,8 @@ try: related_users = seafile_api.get_shared_users_by_repo(repo_id) repo_owner = seafile_api.get_repo_owner(repo_id) - related_users.append(repo_owner) + if repo_owner not in related_users: + related_users.append(repo_owner) record = { 'op_type':'recover', @@ -154,7 +159,7 @@ try: 'repo_name': repo.repo_name, 'path': '/', 'op_user': operator, - 'related_users': [related_users], + 'related_users': related_users, 'org_id': org_id, } diff --git a/seahub/notifications/management/commands/send_work_weixin_notifications.py b/seahub/notifications/management/commands/send_work_weixin_notifications.py new file mode 100644 index 0000000000..6b1bb091ba --- /dev/null +++ b/seahub/notifications/management/commands/send_work_weixin_notifications.py @@ -0,0 +1,183 @@ +# Copyright (c) 2012-2019 Seafile Ltd. +# encoding: utf-8 +from datetime import datetime +import logging +import re +import requests + +from django.core.management.base import BaseCommand +from django.core.urlresolvers import reverse +from django.utils import translation +from django.utils.translation import ungettext + +from seahub.base.models import CommandsLastCheck +from seahub.notifications.models import UserNotification +from seahub.utils import get_site_scheme_and_netloc, get_site_name +from seahub.auth.models import SocialAuthUser +from seahub.work_weixin.utils import work_weixin_notifications_check, \ + get_work_weixin_access_token, handler_work_weixin_api_response +from seahub.work_weixin.settings import WORK_WEIXIN_NOTIFICATIONS_URL, \ + WORK_WEIXIN_PROVIDER, WORK_WEIXIN_UID_PREFIX, WORK_WEIXIN_AGENT_ID + +# Get an instance of a logger +logger = logging.getLogger(__name__) + + +# https://work.weixin.qq.com/api/doc#90000/90135/90236/ +# from social_django.models import UserSocialAuth + +########## Utility Functions ########## +def wrap_div(s): + """ + Replace xx to xx and wrap content with
. + """ + patt = '(.+?)' + + def repl(matchobj): + return matchobj.group(1) + + return '
' + re.sub(patt, repl, s) + '
' + + +class CommandLogMixin(object): + def println(self, msg): + self.stdout.write('[%s] %s\n' % (str(datetime.now()), msg)) + + def log_error(self, msg): + logger.error(msg) + self.println(msg) + + def log_info(self, msg): + logger.info(msg) + self.println(msg) + + def log_debug(self, msg): + logger.debug(msg) + self.println(msg) + + +####################################### + +class Command(BaseCommand, CommandLogMixin): + """ send work weixin notifications + """ + + help = 'Send WeChat Work msg to user if he/she has unseen notices every ' + 'period of time.' + label = "notifications_send_wxwork_notices" + + def handle(self, *args, **options): + self.log_debug('Start sending work weixin msg...') + self.do_action() + self.log_debug('Finish sending work weixin msg.\n') + + def send_work_weixin_msg(self, uid, title, content): + + self.log_info('Send wechat msg to user: %s, msg: %s' % (uid, content)) + + data = { + "touser": uid, + "agentid": WORK_WEIXIN_AGENT_ID, + 'msgtype': 'textcard', + 'textcard': { + 'title': title, + 'description': content, + 'url': self.detail_url, + }, + } + + api_response = requests.post(self.work_weixin_notifications_url, json=data) + api_response_dic = handler_work_weixin_api_response(api_response) + if api_response_dic: + self.log_info(api_response_dic) + else: + self.log_error('can not get work weixin notifications API response') + + def do_action(self): + # check before start + if not work_weixin_notifications_check(): + self.log_error('work weixin notifications settings check failed') + return + + access_token = get_work_weixin_access_token() + if not access_token: + self.log_error('can not get access_token') + + self.work_weixin_notifications_url = WORK_WEIXIN_NOTIFICATIONS_URL + '?access_token=' + access_token + self.detail_url = get_site_scheme_and_netloc().rstrip('/') + reverse('user_notification_list') + site_name = get_site_name() + + # start + now = datetime.now() + today = datetime.now().replace(hour=0).replace(minute=0).replace( + second=0).replace(microsecond=0) + + # 1. get all users who are connected work weixin + socials = SocialAuthUser.objects.filter(provider=WORK_WEIXIN_PROVIDER, uid__contains=WORK_WEIXIN_UID_PREFIX) + users = [(x.username, x.uid[len(WORK_WEIXIN_UID_PREFIX):]) for x in socials] + self.log_info('Found %d users' % len(users)) + if not users: + return + + user_uid_map = {} + for username, uid in users: + user_uid_map[username] = uid + + # 2. get previous time that command last runs + try: + cmd_last_check = CommandsLastCheck.objects.get(command_type=self.label) + self.log_debug('Last check time is %s' % cmd_last_check.last_check) + + last_check_dt = cmd_last_check.last_check + + cmd_last_check.last_check = now + cmd_last_check.save() + except CommandsLastCheck.DoesNotExist: + last_check_dt = today + self.log_debug('Create new last check time: %s' % now) + CommandsLastCheck(command_type=self.label, last_check=now).save() + + # 3. get all unseen notices for those users + qs = UserNotification.objects.filter( + timestamp__gt=last_check_dt + ).filter(seen=False).filter( + to_user__in=user_uid_map.keys() + ) + self.log_info('Found %d notices' % qs.count()) + if qs.count() == 0: + return + + user_notices = {} + for q in qs: + if q.to_user not in user_notices: + user_notices[q.to_user] = [q] + else: + user_notices[q.to_user].append(q) + + # save current language + cur_language = translation.get_language() + # active zh-cn + translation.activate('zh-cn') + self.log_info('the language is set to zh-cn') + + # 4. send msg to users + for username, uid in users: + notices = user_notices.get(username, []) + count = len(notices) + if count == 0: + continue + + title = ungettext( + "\n" + "You've got 1 new notice on %(site_name)s:\n", + "\n" + "You've got %(num)s new notices on %(site_name)s:\n", + count + ) % {'num': count, 'site_name': site_name, } + + content = ''.join([wrap_div(x.format_msg()) for x in notices]) + self.send_work_weixin_msg(uid, title, content) + + # reset language + translation.activate(cur_language) + self.log_info('reset language success') diff --git a/seahub/notifications/management/commands/send_wxwork_notices.py b/seahub/notifications/management/commands/send_wxwork_notices.py index c6e6125086..537f8660fc 100644 --- a/seahub/notifications/management/commands/send_wxwork_notices.py +++ b/seahub/notifications/management/commands/send_wxwork_notices.py @@ -51,6 +51,9 @@ class CommandLogMixin(object): ####################################### class Command(BaseCommand, CommandLogMixin): + """ please use send_work_weixin_notifications.py + """ + help = 'Send WeChat Work msg to user if he/she has unseen notices every ' 'period of time.' label = "notifications_send_wxwork_notices" diff --git a/seahub/settings.py b/seahub/settings.py index 3ad42873f0..206511865d 100644 --- a/seahub/settings.py +++ b/seahub/settings.py @@ -306,6 +306,8 @@ ENABLE_WATERMARK = False ENABLE_WORK_WEIXIN_OAUTH = False # allow seafile admin import user from work weixin ENABLE_WORK_WEIXIN_DEPARTMENTS = False +# allow send unread msg to work weixin +ENABLE_WORK_WEIXIN_NOTIFICATIONS = False # allow user to clean library trash ENABLE_USER_CLEAN_TRASH = True diff --git a/seahub/templates/repo_snapshot_react.html b/seahub/templates/repo_snapshot_react.html new file mode 100644 index 0000000000..ffd22535a1 --- /dev/null +++ b/seahub/templates/repo_snapshot_react.html @@ -0,0 +1,34 @@ +{% extends 'base_for_react.html' %} +{% load seahub_tags i18n avatar_tags %} +{% load render_bundle from webpack_loader %} + +{% block sub_title %}{% trans "Snapshot" %} - {% endblock %} + +{% block extra_style %} +{% render_bundle 'repoSnapshot' 'css' %} +{% endblock %} + +{% block extra_script %} + +{% render_bundle 'repoSnapshot' 'js' %} +{% endblock %} diff --git a/seahub/urls.py b/seahub/urls.py index 106ee0f328..ba519177db 100644 --- a/seahub/urls.py +++ b/seahub/urls.py @@ -14,7 +14,7 @@ from seahub.views.file import view_history_file, view_trash_file,\ text_diff, view_raw_file, download_file, view_lib_file, \ file_access, view_lib_file_via_smart_link, view_media_file_via_share_link, \ view_media_file_via_public_wiki -from seahub.views.repo import repo_history_view, view_shared_dir, \ +from seahub.views.repo import repo_history_view, repo_snapshot, view_shared_dir, \ view_shared_upload_link, view_lib_as_wiki from notifications.views import notification_list from seahub.views.wiki import personal_wiki, personal_wiki_pages, \ @@ -55,6 +55,7 @@ from seahub.api2.endpoints.file_tag import FileTagView from seahub.api2.endpoints.file_tag import FileTagsView from seahub.api2.endpoints.repo_trash import RepoTrash from seahub.api2.endpoints.repo_commit_dir import RepoCommitDirView +from seahub.api2.endpoints.repo_commit_revert import RepoCommitRevertView from seahub.api2.endpoints.deleted_repos import DeletedRepos from seahub.api2.endpoints.repo_history import RepoHistory from seahub.api2.endpoints.repo_set_password import RepoSetPassword @@ -172,6 +173,7 @@ urlpatterns = [ url(r'^repo/text_diff/(?P[-0-9a-f]{36})/$', text_diff, name='text_diff'), url(r'^repo/history/(?P[-0-9a-f]{36})/$', repo_history, name='repo_history'), url(r'^repo/history/view/(?P[-0-9a-f]{36})/$', repo_history_view, name='repo_history_view'), + url(r'^repo/(?P[-0-9a-f]{36})/snapshot/$', repo_snapshot, name="repo_snapshot"), url(r'^repo/recycle/(?P[-0-9a-f]{36})/$', repo_recycle_view, name='repo_recycle_view'), url(r'^dir/recycle/(?P[-0-9a-f]{36})/$', dir_recycle_view, name='dir_recycle_view'), url(r'^repo/(?P[-0-9a-f]{36})/trash/$', repo_folder_trash, name="repo_folder_trash"), @@ -336,6 +338,7 @@ urlpatterns = [ url(r'^api/v2.1/repos/(?P[-0-9a-f]{36})/file/new_history/$', NewFileHistoryView.as_view(), name='api-v2.1-new-file-history-view'), url(r'^api/v2.1/repos/(?P[-0-9a-f]{36})/dir/$', DirView.as_view(), name='api-v2.1-dir-view'), url(r'^api/v2.1/repos/(?P[-0-9a-f]{36})/commits/(?P[0-9a-f]{40})/dir/$', RepoCommitDirView.as_view(), name='api-v2.1-repo-commit-dir'), + url(r'^api/v2.1/repos/(?P[-0-9a-f]{36})/commits/(?P[0-9a-f]{40})/revert/$', RepoCommitRevertView.as_view(), name='api-v2.1-repo-commit-revert'), url(r'^api/v2.1/repos/(?P[-0-9a-f]{36})/dir/detail/$', DirDetailView.as_view(), name='api-v2.1-dir-detail-view'), url(r'^api/v2.1/repos/(?P[-0-9a-f]{36})/trash/$', RepoTrash.as_view(), name='api-v2.1-repo-trash'), url(r'^api/v2.1/repos/(?P[-0-9a-f]{36})/history/$', RepoHistory.as_view(), name='api-v2.1-repo-history'), diff --git a/seahub/views/repo.py b/seahub/views/repo.py index e7908c6531..027cc54ad3 100644 --- a/seahub/views/repo.py +++ b/seahub/views/repo.py @@ -153,6 +153,50 @@ def repo_history_view(request, repo_id): 'referer': referer, }) +@login_required +def repo_snapshot(request, repo_id): + """View repo in history. + """ + repo = get_repo(repo_id) + if not repo: + raise Http404 + + username = request.user.username + user_perm = check_folder_permission(request, repo.id, '/') + if user_perm is None: + return render_error(request, _(u'Permission denied')) + + try: + server_crypto = UserOptions.objects.is_server_crypto(username) + except CryptoOptionNotSetError: + # Assume server_crypto is ``False`` if this option is not set. + server_crypto = False + + reverse_url = reverse('lib_view', args=[repo_id, repo.name, '']) + if repo.encrypted and \ + (repo.enc_version == 1 or (repo.enc_version == 2 and server_crypto)) \ + and not is_password_set(repo.id, username): + return render(request, 'decrypt_repo_form.html', { + 'repo': repo, + 'next': get_next_url_from_request(request) or reverse_url, + }) + + commit_id = request.GET.get('commit_id', None) + if commit_id is None: + return HttpResponseRedirect(reverse_url) + current_commit = get_commit(repo.id, repo.version, commit_id) + if not current_commit: + current_commit = get_commit(repo.id, repo.version, repo.head_cmmt_id) + + repo_owner = seafile_api.get_repo_owner(repo.id) + is_repo_owner = True if username == repo_owner else False + + return render(request, 'repo_snapshot_react.html', { + 'repo': repo, + "is_repo_owner": is_repo_owner, + 'current_commit': current_commit, + }) + @login_required def view_lib_as_wiki(request, repo_id, path): diff --git a/seahub/wopi/views.py b/seahub/wopi/views.py index b42c482ff5..e8692aad09 100644 --- a/seahub/wopi/views.py +++ b/seahub/wopi/views.py @@ -213,7 +213,7 @@ class WOPIFilesView(APIView): # new file creation feature is not implemented on wopi host(seahub) # hide save as button on view/edit file page - result['UserCanNotWriteRelative'] = False + result['UserCanNotWriteRelative'] = True return HttpResponse(json.dumps(result), status=200, content_type=json_content_type) diff --git a/seahub/work_weixin/settings.py b/seahub/work_weixin/settings.py index 3a5045f8d9..f3623a8e3a 100644 --- a/seahub/work_weixin/settings.py +++ b/seahub/work_weixin/settings.py @@ -19,7 +19,7 @@ WORK_WEIXIN_DEPARTMENT_MEMBERS_URL = getattr(settings, 'WORK_WEIXIN_DEPARTMENT_M WORK_WEIXIN_AGENT_ID = getattr(settings, 'WORK_WEIXIN_AGENT_ID', '') ENABLE_WORK_WEIXIN_OAUTH = getattr(settings, 'ENABLE_WORK_WEIXIN_OAUTH', False) WORK_WEIXIN_UID_PREFIX = WORK_WEIXIN_CORP_ID + '_' -AUTO_UPDATE_WORK_WEIXIN_USER_INFO = getattr(settings, 'AUTO_UPDATE_WORK_WEIXIN_USER_INFO', False) +WORK_WEIXIN_USER_INFO_AUTO_UPDATE = getattr(settings, 'WORK_WEIXIN_USER_INFO_AUTO_UPDATE', True) WORK_WEIXIN_AUTHORIZATION_URL = getattr(settings, 'WORK_WEIXIN_AUTHORIZATION_URL', 'https://open.work.weixin.qq.com/wwopen/sso/qrConnect') WORK_WEIXIN_GET_USER_INFO_URL = getattr(settings, 'WORK_WEIXIN_GET_USER_INFO_URL', @@ -27,6 +27,11 @@ WORK_WEIXIN_GET_USER_INFO_URL = getattr(settings, 'WORK_WEIXIN_GET_USER_INFO_URL WORK_WEIXIN_GET_USER_PROFILE_URL = getattr(settings, 'WORK_WEIXIN_GET_USER_PROFILE_URL', 'https://qyapi.weixin.qq.com/cgi-bin/user/get') +# # work weixin notifications +ENABLE_WORK_WEIXIN_NOTIFICATIONS = getattr(settings, 'ENABLE_WORK_WEIXIN_NOTIFICATIONS', False) +WORK_WEIXIN_NOTIFICATIONS_URL = getattr(settings, 'WORK_WEIXIN_NOTIFICATIONS_URL', + 'https://qyapi.weixin.qq.com/cgi-bin/message/send') + # # constants WORK_WEIXIN_PROVIDER = 'work-weixin' diff --git a/seahub/work_weixin/utils.py b/seahub/work_weixin/utils.py index c2e8a59dd2..feb27c9258 100644 --- a/seahub/work_weixin/utils.py +++ b/seahub/work_weixin/utils.py @@ -11,7 +11,8 @@ from seahub.work_weixin.settings import WORK_WEIXIN_CORP_ID, WORK_WEIXIN_AGENT_S WORK_WEIXIN_ACCESS_TOKEN_URL, ENABLE_WORK_WEIXIN_DEPARTMENTS, \ WORK_WEIXIN_DEPARTMENTS_URL, WORK_WEIXIN_DEPARTMENT_MEMBERS_URL, \ ENABLE_WORK_WEIXIN_OAUTH, WORK_WEIXIN_AGENT_ID, WORK_WEIXIN_AUTHORIZATION_URL, \ - WORK_WEIXIN_GET_USER_INFO_URL, WORK_WEIXIN_GET_USER_PROFILE_URL + WORK_WEIXIN_GET_USER_INFO_URL, WORK_WEIXIN_GET_USER_PROFILE_URL, \ + ENABLE_WORK_WEIXIN_NOTIFICATIONS, WORK_WEIXIN_NOTIFICATIONS_URL from seahub.profile.models import Profile logger = logging.getLogger(__name__) @@ -23,6 +24,8 @@ WORK_WEIXIN_ACCESS_TOKEN_CACHE_KEY = 'WORK_WEIXIN_ACCESS_TOKEN' def get_work_weixin_access_token(): + """ get global work weixin access_token + """ cache_key = normalize_cache_key(WORK_WEIXIN_ACCESS_TOKEN_CACHE_KEY) access_token = cache.get(cache_key, None) @@ -45,6 +48,8 @@ def get_work_weixin_access_token(): def handler_work_weixin_api_response(response): + """ handler work_weixin response and errcode + """ try: response = response.json() except ValueError: @@ -59,6 +64,8 @@ def handler_work_weixin_api_response(response): def work_weixin_base_check(): + """ work weixin base check + """ if not WORK_WEIXIN_CORP_ID or not WORK_WEIXIN_AGENT_SECRET or not WORK_WEIXIN_ACCESS_TOKEN_URL: logger.error('work weixin base relevant settings invalid.') logger.error('WORK_WEIXIN_CORP_ID: %s' % WORK_WEIXIN_CORP_ID) @@ -69,13 +76,14 @@ def work_weixin_base_check(): def work_weixin_oauth_check(): - if not work_weixin_base_check(): - return False - + """ use for work weixin login and profile bind + """ if not ENABLE_WORK_WEIXIN_OAUTH: - logger.error('work weixin oauth not enabled.') return False else: + if not work_weixin_base_check(): + return False + if not WORK_WEIXIN_AGENT_ID \ or not WORK_WEIXIN_GET_USER_INFO_URL \ or not WORK_WEIXIN_AUTHORIZATION_URL \ @@ -91,13 +99,14 @@ def work_weixin_oauth_check(): def admin_work_weixin_departments_check(): - if not work_weixin_base_check(): - return False - + """ use for admin work weixin departments + """ if not ENABLE_WORK_WEIXIN_DEPARTMENTS: - logger.error('admin work weixin departments not enabled.') return False else: + if not work_weixin_base_check(): + return False + if not WORK_WEIXIN_DEPARTMENTS_URL \ or not WORK_WEIXIN_DEPARTMENT_MEMBERS_URL: logger.error('admin work weixin departments relevant settings invalid.') @@ -108,12 +117,48 @@ def admin_work_weixin_departments_check(): return True +def work_weixin_notifications_check(): + """ use for send work weixin notifications + """ + if not ENABLE_WORK_WEIXIN_NOTIFICATIONS: + return False + else: + if not work_weixin_base_check(): + return False + + if not WORK_WEIXIN_AGENT_ID \ + or not WORK_WEIXIN_NOTIFICATIONS_URL: + logger.error('work weixin notifications relevant settings invalid.') + logger.error('WORK_WEIXIN_AGENT_ID: %s' % WORK_WEIXIN_AGENT_ID) + logger.error('WORK_WEIXIN_NOTIFICATIONS_URL: %s' % WORK_WEIXIN_NOTIFICATIONS_URL) + return False + + return True + + def update_work_weixin_user_info(api_user): - email = api_user.get('username') - try: - # update additional user info - nickname = api_user.get("name", None) - if nickname is not None: - Profile.objects.add_or_update(email, nickname) - except Exception as e: - logger.error(e) + """ update user profile from work weixin + + use for work weixin departments, login, profile bind + """ + # update additional user info + username = api_user.get('username') + nickname = api_user.get('name') + contact_email = api_user.get('contact_email') + + # make sure the contact_email is unique + if contact_email and Profile.objects.get_profile_by_contact_email(contact_email): + logger.warning('contact email %s already exists' % contact_email) + contact_email = '' + + profile_kwargs = {} + if nickname: + profile_kwargs['nickname'] = nickname + if contact_email: + profile_kwargs['contact_email'] = contact_email + + if profile_kwargs: + try: + Profile.objects.add_or_update(username, **profile_kwargs) + except Exception as e: + logger.error(e) diff --git a/seahub/work_weixin/views.py b/seahub/work_weixin/views.py index eb24c39248..133fded218 100644 --- a/seahub/work_weixin/views.py +++ b/seahub/work_weixin/views.py @@ -17,7 +17,7 @@ from seahub.base.accounts import User from seahub.work_weixin.settings import WORK_WEIXIN_AUTHORIZATION_URL, WORK_WEIXIN_CORP_ID, \ WORK_WEIXIN_AGENT_ID, WORK_WEIXIN_PROVIDER, \ WORK_WEIXIN_GET_USER_INFO_URL, WORK_WEIXIN_GET_USER_PROFILE_URL, WORK_WEIXIN_UID_PREFIX, \ - AUTO_UPDATE_WORK_WEIXIN_USER_INFO + WORK_WEIXIN_USER_INFO_AUTO_UPDATE from seahub.work_weixin.utils import work_weixin_oauth_check, get_work_weixin_access_token, \ handler_work_weixin_api_response, update_work_weixin_user_info from seahub.utils.auth import gen_user_virtual_id, VIRTUAL_ID_EMAIL_DOMAIN @@ -100,8 +100,8 @@ def work_weixin_oauth_callback(request): return render_error( request, _('Error, new user registration is not allowed, please contact administrator.')) - if is_new_user or AUTO_UPDATE_WORK_WEIXIN_USER_INFO: - # update user info + # update user info + if is_new_user or WORK_WEIXIN_USER_INFO_AUTO_UPDATE: user_info_data = { 'access_token': access_token, 'userid': user_id, @@ -193,8 +193,8 @@ def work_weixin_oauth_connect_callback(request): SocialAuthUser.objects.add(email, WORK_WEIXIN_PROVIDER, uid) - if AUTO_UPDATE_WORK_WEIXIN_USER_INFO: - # update user info + # update user info + if WORK_WEIXIN_USER_INFO_AUTO_UPDATE: user_info_data = { 'access_token': access_token, 'userid': user_id, diff --git a/tests/api/endpoints/test_repo_commit_revert.py b/tests/api/endpoints/test_repo_commit_revert.py new file mode 100644 index 0000000000..9426f27daf --- /dev/null +++ b/tests/api/endpoints/test_repo_commit_revert.py @@ -0,0 +1,111 @@ +import os +import json + +from django.core.urlresolvers import reverse + +from seaserv import seafile_api +from seahub.test_utils import BaseTestCase + + +class RepoCommitRevertTest(BaseTestCase): + + def setUp(self): + self.user_name = self.user.username + self.admin_name = self.admin.username + + self.repo_id = self.repo.id + self.repo_name = self.repo.repo_name + + self.file_path = self.file + self.file_name = os.path.basename(self.file_path) + + self.enc_repo_id = self.enc_repo.id + self.enc_repo_name = self.enc_repo + + self.folder_path = self.folder + self.folder_name = os.path.basename(self.folder.rstrip('/')) + + def tearDown(self): + self.remove_repo() + self.remove_group() + + def test_post(self): + # delete a file first + seafile_api.del_file(self.repo_id, '/', self.file_name, self.user_name) + + self.login_as(self.user) + + # get commit id form trash + trash_url = reverse('api-v2.1-repo-trash', args=[self.repo_id]) + trash_resp = self.client.get(trash_url) + self.assertEqual(200, trash_resp.status_code) + trash_json_resp = json.loads(trash_resp.content) + assert trash_json_resp['data'][0]['obj_name'] == self.file_name + assert not trash_json_resp['data'][0]['is_dir'] + assert trash_json_resp['data'][0]['commit_id'] + commit_id = trash_json_resp['data'][0]['commit_id'] + + dir_url = reverse('api-v2.1-dir-view', args=[self.repo_id]) + url = reverse('api-v2.1-repo-commit-revert', args=[self.repo_id, commit_id]) + + # test can post + resp = self.client.get(dir_url) + self.assertEqual(200, resp.status_code) + json_resp = json.loads(resp.content) + assert len(json_resp['dirent_list']) == 1 + + resp = self.client.post(url) + self.assertEqual(200, resp.status_code) + + resp = self.client.get(dir_url) + self.assertEqual(200, resp.status_code) + json_resp = json.loads(resp.content) + assert len(json_resp['dirent_list']) == 2 + + # test_can_not_post_with_invalid_repo_permission + self.logout() + self.login_as(self.admin) + + resp = self.client.post(url) + self.assertEqual(403, resp.status_code) + + def test_enc_repo_post(self): + # delete a file first + seafile_api.del_file(self.enc_repo_id, '/', self.file_name, self.user_name) + + self.login_as(self.user) + + # get commit id form trash + trash_url = reverse('api-v2.1-repo-trash', args=[self.enc_repo_id]) + trash_resp = self.client.get(trash_url) + self.assertEqual(200, trash_resp.status_code) + trash_json_resp = json.loads(trash_resp.content) + assert trash_json_resp['data'][0]['obj_name'] == self.file_name + assert not trash_json_resp['data'][0]['is_dir'] + assert trash_json_resp['data'][0]['commit_id'] + commit_id = trash_json_resp['data'][0]['commit_id'] + + dir_url = reverse('api-v2.1-dir-view', args=[self.enc_repo_id]) + url = reverse('api-v2.1-repo-commit-revert', args=[self.enc_repo_id, commit_id]) + + # test can not post without repo decrypted + resp = self.client.post(url) + self.assertEqual(403, resp.status_code) + + resp = self.client.get(dir_url) + self.assertEqual(200, resp.status_code) + json_resp = json.loads(resp.content) + assert len(json_resp['dirent_list']) == 0 + + # test can post with repo decrypted + decrypted_url = reverse('api-v2.1-repo-set-password', args=[self.enc_repo_id]) + resp = self.client.post(decrypted_url, data={'password': '123'}) + self.assertEqual(200, resp.status_code) + + resp = self.client.post(url) + self.assertEqual(200, resp.status_code) + + resp = self.client.get(dir_url) + self.assertEqual(200, resp.status_code) + json_resp = json.loads(resp.content) + assert len(json_resp['dirent_list']) == 1 diff --git a/tests/api/endpoints/test_repos_batch.py b/tests/api/endpoints/test_repos_batch.py index 489da5a48f..10036efab2 100644 --- a/tests/api/endpoints/test_repos_batch.py +++ b/tests/api/endpoints/test_repos_batch.py @@ -994,6 +994,36 @@ class ReposAsyncBatchMoveItemView(BaseTestCase): resp = self.client.post(self.url, json.dumps(data), 'application/json') self.assertEqual(403, resp.status_code) + def test_move_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 = { + "src_repo_id": admin_repo_id, + "src_parent_dir": '/', + "src_dirents":[admin_file_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'] == 'File %s is locked.' % admin_file_name class ReposSyncBatchCopyItemView(BaseTestCase): @@ -1539,3 +1569,34 @@ class ReposSyncBatchMoveItemView(BaseTestCase): } resp = self.client.post(self.url, json.dumps(data), 'application/json') self.assertEqual(403, resp.status_code) + + def test_move_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 = { + "src_repo_id": admin_repo_id, + "src_parent_dir": '/', + "src_dirents":[admin_file_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'] == 'File %s is locked.' % admin_file_name