diff --git a/frontend/package-lock.json b/frontend/package-lock.json index f9f20618be..ba09c63344 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -15924,9 +15924,9 @@ } }, "seafile-js": { - "version": "0.2.101", - "resolved": "https://registry.npmjs.org/seafile-js/-/seafile-js-0.2.101.tgz", - "integrity": "sha512-CqF+4FKnUhnUYKCjiVbp10GkI9Ej+d4UfaWRvdGNaR74tK+0D9dUq7RNXUGS/MpIAlif7hbz5k+LUFafkfGmNg==", + "version": "0.2.106", + "resolved": "https://registry.npmjs.org/seafile-js/-/seafile-js-0.2.106.tgz", + "integrity": "sha512-lT2gLD8hz75rlc7PW4kwW9SWe2JUywsDr5EDYYdQoPUz437i0YEq5wMIJq8jcOHaTSqgwbIfrWpVmOIs8Lkilg==", "requires": { "axios": "^0.18.0", "form-data": "^2.3.2", diff --git a/frontend/package.json b/frontend/package.json index 03eafb9173..ba51651d87 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.101", + "seafile-js": "^0.2.106", "socket.io-client": "^2.2.0", "sw-precache-webpack-plugin": "0.11.4", "unified": "^7.0.0", diff --git a/frontend/src/components/dialog/generate-share-link.js b/frontend/src/components/dialog/generate-share-link.js index c6929b5378..d03de0404b 100644 --- a/frontend/src/components/dialog/generate-share-link.js +++ b/frontend/src/components/dialog/generate-share-link.js @@ -41,6 +41,7 @@ class GenerateShareLink extends React.Component { 'can_download': true }; this.isExpireDaysNoLimit = (parseInt(shareLinkExpireDaysMin) === 0 && parseInt(shareLinkExpireDaysMax) === 0); + this.isOfficeFile = Utils.isOfficeFile(this.props.itemPath); } componentDidMount() { @@ -57,12 +58,13 @@ class GenerateShareLink extends React.Component { this.setState({isLoading: false}); } }); - - seafileAPI.getFileInfo(repoID, path).then((res) => { - if (res.data) { - this.setState({fileInfo: res.data}); - } - }); + if (this.isOfficeFile) { + seafileAPI.getFileInfo(repoID, path).then((res) => { + if (res.data) { + this.setState({fileInfo: res.data}); + } + }); + } } onPasswordInputChecked = () => { @@ -402,7 +404,7 @@ class GenerateShareLink extends React.Component { this.setPermission('preview')} />{' '}{gettext('Preview only')} - {(Utils.isOfficeFile(this.props.itemPath) && fileInfo && fileInfo.can_edit) && + {(this.isOfficeFile && fileInfo && fileInfo.can_edit) && + {sharedUploadInfo.expire_date && ( + +
{gettext('Expiration Date:')}
+
{moment(sharedUploadInfo.expire_date).format('YYYY-MM-DD hh:mm:ss')}
+
+ )} {canSendShareLinkEmail && !isSendLinkShown && } {!isSendLinkShown && } @@ -192,6 +233,18 @@ class GenerateUploadLink extends React.Component { } + + + + {this.state.isExpireChecked && + + + + } {this.state.errorInfo && {this.state.errorInfo}} diff --git a/frontend/src/components/dialog/leave-group-dialog.js b/frontend/src/components/dialog/leave-group-dialog.js new file mode 100644 index 0000000000..c5caf9997f --- /dev/null +++ b/frontend/src/components/dialog/leave-group-dialog.js @@ -0,0 +1,43 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { gettext, username } from '../../utils/constants'; +import { seafileAPI } from '../../utils/seafile-api'; +import { Modal, ModalHeader, ModalBody, ModalFooter, Button } from 'reactstrap'; + +class LeaveGroupDialog extends React.Component { + + constructor(props) { + super(props); + } + + leaveGroup = () => { + seafileAPI.quitGroup(this.props.groupID, username).then((res)=> { + this.props.onGroupChanged(); + }); + } + + render() { + return( + + {gettext('Leave Group')} + +

{gettext('Really want to leave this group?')}

+
+ + + + +
+ ); + } +} + +const LeaveGroupDialogPropTypes = { + toggleLeaveGroupDialog: PropTypes.func.isRequired, + groupID: PropTypes.string.isRequired, + onGroupChanged: PropTypes.func.isRequired, +}; + +LeaveGroupDialog.propTypes = LeaveGroupDialogPropTypes; + +export default LeaveGroupDialog; diff --git a/frontend/src/components/file-uploader/file-uploader.js b/frontend/src/components/file-uploader/file-uploader.js index da30a6ad22..ab6b980e47 100644 --- a/frontend/src/components/file-uploader/file-uploader.js +++ b/frontend/src/components/file-uploader/file-uploader.js @@ -128,43 +128,31 @@ class FileUploader extends React.Component { this.resumable.on('dragstart', this.onDragStart.bind(this)); } - onChunkingComplete = (file) => { - if (file.relativePath !== file.fileName) { - return; // is upload a folder; - } - if (enableResumableFileUpload) { - let repoID = this.props.repoID; - seafileAPI.getFileUploadedBytes(repoID, this.props.path, file.fileName).then(res => { - let uploadedBytes = res.data.uploadedBytes; - let offset = Math.floor(uploadedBytes / (1024 * 1024)); - file.markChunksCompleted(offset); - }); - } - } - - onFileAdded = (resumableFile, files) => { - //get parent_dir、relative_path; + onChunkingComplete = (resumableFile) => { + //get parent_dir relative_path let path = this.props.path === '/' ? '/' : this.props.path + '/'; let fileName = resumableFile.fileName; let relativePath = resumableFile.relativePath; let isFile = fileName === relativePath; - //update formdata; + //update formdata resumableFile.formData = {}; - if (isFile) { + if (isFile) { // upload file resumableFile.formData = { parent_dir: path, }; - } else { + } else { // upload folder let relative_path = relativePath.slice(0, relativePath.lastIndexOf('/') + 1); resumableFile.formData = { parent_dir: path, relative_path: relative_path }; } + } - //check repetition - //uploading is file and only upload one file + onFileAdded = (resumableFile, files) => { + let isFile = resumableFile.fileName === resumableFile.relativePath; + // uploading is file and only upload one file if (isFile && files.length === 1) { let hasRepetition = false; let direntList = this.props.direntList; @@ -181,14 +169,28 @@ class FileUploader extends React.Component { }); } else { this.setUploadFileList(this.resumable.files); + this.resumableUpload(resumableFile); + } + } else { + this.setUploadFileList(this.resumable.files); + if (isFile) { + this.resumableUpload(resumableFile); + } else { this.resumable.upload(); } - } else { - this.setUploadFileList(this.resumable.files); - this.resumable.upload(); } } + resumableUpload = (resumableFile) => { + let { repoID, path } = this.props; + seafileAPI.getFileUploadedBytes(repoID, path, resumableFile.fileName).then(res => { + let uploadedBytes = res.data.uploadedBytes; + let offset = Math.floor(uploadedBytes / (1024 * 1024)); + resumableFile.markChunksCompleted(offset); + this.resumable.upload(); + }); + } + filesAddedComplete = (resumable, files) => { // single file uploading can check repetition, because custom dialog conn't prevent program execution; } @@ -416,7 +418,7 @@ class FileUploader extends React.Component { this.uploadInput.current.removeAttribute('webkitdirectory'); let repoID = this.props.repoID; seafileAPI.getUploadLink(repoID, this.props.path).then(res => { - this.resumable.opts.target = res.data; + this.resumable.opts.target = res.data + '?ret-json=1'; if (Utils.isIEBrower()) { this.uploadInput.current.click(); } @@ -430,7 +432,7 @@ class FileUploader extends React.Component { this.uploadInput.current.setAttribute('webkitdirectory', 'webkitdirectory'); let repoID = this.props.repoID; seafileAPI.getUploadLink(repoID, this.props.path).then(res => { - this.resumable.opts.target = res.data; + this.resumable.opts.target = res.data + '?ret-json=1'; if (Utils.isIEBrower()) { this.uploadInput.current.click(); } @@ -444,7 +446,7 @@ class FileUploader extends React.Component { let repoID = this.props.repoID; this.uploadInput.current.setAttribute('webkitdirectory', 'webkitdirectory'); seafileAPI.getUploadLink(repoID, this.props.path).then(res => { - this.resumable.opts.target = res.data; + this.resumable.opts.target = res.data + '?ret-json=1'; }); } diff --git a/frontend/src/components/search/search.js b/frontend/src/components/search/search.js index 6e9674b5c7..e6d273b3d6 100644 --- a/frontend/src/components/search/search.js +++ b/frontend/src/components/search/search.js @@ -1,12 +1,14 @@ import React, { Component, Fragment } from 'react'; import PropTypes from 'prop-types'; import MediaQuery from 'react-responsive'; +import { seafileAPI } from '../../utils/seafile-api'; import { gettext, siteRoot } from '../../utils/constants'; import SearchResultItem from './search-result-item'; import editorUtilities from '../../utils/editor-utilties'; import More from '../more'; const propTypes = { + isPublic: PropTypes.bool, repoID: PropTypes.string, placeholder: PropTypes.string, onSearchedClick: PropTypes.func.isRequired, @@ -93,27 +95,53 @@ class Search extends Component { sendRequest(queryData, cancelToken) { var _this = this; - editorUtilities.searchFiles(queryData,cancelToken).then(res => { - if (!res.data.total) { + let isPublic = this.props.isPublic; + + if (isPublic) { + seafileAPI.searchFilesInPublishedRepo(queryData.q, queryData.search_repo).then(res => { + if (!res.data.total) { + _this.setState({ + resultItems: [], + isResultGetted: true + }); + _this.source = null; + return; + } + + let items = _this.formatResultItems(res.data.results); _this.setState({ - resultItems: [], + resultItems: items, isResultGetted: true }); _this.source = null; - return; - } - - let items = _this.formatResultItems(res.data.results); - _this.setState({ - resultItems: items, - isResultGetted: true + }).catch(res => { + /* eslint-disable */ + console.log(res); + /* eslint-enable */ }); - _this.source = null; - }).catch(res => { - /* eslint-disable */ - console.log(res); - /* eslint-enable */ - }); + } else { + editorUtilities.searchFiles(queryData,cancelToken).then(res => { + if (!res.data.total) { + _this.setState({ + resultItems: [], + isResultGetted: true + }); + _this.source = null; + return; + } + + let items = _this.formatResultItems(res.data.results); + _this.setState({ + resultItems: items, + isResultGetted: true + }); + _this.source = null; + }).catch(res => { + /* eslint-disable */ + console.log(res); + /* eslint-enable */ + }); + } } cancelRequest() { diff --git a/frontend/src/models/shared-upload-info.js b/frontend/src/models/shared-upload-info.js index e9776e62e9..cd7f0f9b8c 100644 --- a/frontend/src/models/shared-upload-info.js +++ b/frontend/src/models/shared-upload-info.js @@ -10,6 +10,8 @@ class SharedUploadInfo { this.ctime = object.ctime; this.token = object.token; this.view_cnt = object.view_cnt; + this.expire_date = object.expire_date; + this.is_expired = object.is_expired; } } diff --git a/frontend/src/pages/groups/group-view.js b/frontend/src/pages/groups/group-view.js index b1ee881984..a72869ab34 100644 --- a/frontend/src/pages/groups/group-view.js +++ b/frontend/src/pages/groups/group-view.js @@ -20,6 +20,7 @@ import RenameGroupDialog from '../../components/dialog/rename-group-dialog'; import TransferGroupDialog from '../../components/dialog/transfer-group-dialog'; // import ImportMembersDialog from '../../components/dialog/import-members-dialog'; import ManageMembersDialog from '../../components/dialog/manage-members-dialog'; +import LeaveGroupDialog from '../../components/dialog/leave-group-dialog'; import SharedRepoListView from '../../components/shared-repo-list-view/shared-repo-list-view'; import LibDetail from '../../components/dirent-detail/lib-details'; @@ -60,6 +61,7 @@ class GroupView extends React.Component { showManageMembersDialog: false, groupMembers: [], isShowDetails: false, + isLeaveGroupDialogOpen: false, }; } @@ -307,6 +309,13 @@ class GroupView extends React.Component { }); } + toggleLeaveGroupDialog = () => { + this.setState({ + isLeaveGroupDialogOpen: !this.state.isLeaveGroupDialogOpen, + showGroupDropdown: false, + }); + } + listGroupMembers = () => { seafileAPI.listGroupMembers(this.props.groupID).then((res) => { this.setState({ @@ -407,7 +416,7 @@ class GroupView extends React.Component { )}
- { (isShowSettingIcon && this.state.isStaff) && + { isShowSettingIcon && @@ -419,6 +428,7 @@ class GroupView extends React.Component { onClick={this.toggleGroupDropdown}>
+ {(this.state.isStaff || this.state.isOwner) && + } + {(this.state.isStaff || this.state.isOwner) && + } { this.state.isOwner && } + {/* gourp owner only can dissmiss group, admin could not quit, department member could not quit */} + {(!this.state.isOwner && !this.state.isStaff && !isDepartmentGroup) && + + }
@@ -557,6 +576,13 @@ class GroupView extends React.Component { isOwner={this.state.isOwner} /> } + {this.state.isLeaveGroupDialogOpen && + + } ); } diff --git a/frontend/src/pages/share-admin/upload-links.js b/frontend/src/pages/share-admin/upload-links.js index 97d3efb2ae..130d26b7e3 100644 --- a/frontend/src/pages/share-admin/upload-links.js +++ b/frontend/src/pages/share-admin/upload-links.js @@ -1,5 +1,6 @@ -import React, { Component } from 'react'; +import React, { Component, Fragment } from 'react'; import { Link } from '@reach/router'; +import moment from 'moment'; import { Modal, ModalHeader, ModalBody } from 'reactstrap'; import { gettext, siteRoot, loginUrl, canGenerateShareLink } from '../../utils/constants'; import { seafileAPI } from '../../utils/seafile-api'; @@ -52,9 +53,10 @@ class Content extends Component { {/*icon*/} - {gettext('Name')} - {gettext('Library')} - {gettext('Visits')} + {gettext('Name')} + {gettext('Library')} + {gettext('Visits')} + {gettext('Expiration')} {/*Operations*/} @@ -114,6 +116,24 @@ class Item extends Component { return { iconUrl, uploadUrl }; } + renderExpriedData = () => { + let item = this.props.item; + if (!item.expire_date) { + return ( + -- + ); + } + let expire_date = moment(item.expire_date).format('YYYY-MM-DD'); + return ( + + {item.is_expired ? + {expire_date} : + expire_date + } + + ); + } + render() { let item = this.props.item; let { iconUrl, uploadUrl } = this.getUploadParams(); @@ -128,6 +148,7 @@ class Item extends Component { {item.obj_name} {item.repo_name} {item.view_cnt} + {this.renderExpriedData()} diff --git a/frontend/src/pages/sys-admin/side-panel.js b/frontend/src/pages/sys-admin/side-panel.js index 449e5f6488..8dbb63328d 100644 --- a/frontend/src/pages/sys-admin/side-panel.js +++ b/frontend/src/pages/sys-admin/side-panel.js @@ -5,7 +5,7 @@ import Logo from '../../components/logo'; import { gettext, siteRoot, isPro, isDefaultAdmin, canViewSystemInfo, canViewStatistic, canConfigSystem, canManageLibrary, canManageUser, canManageGroup, canViewUserLog, canViewAdminLog, constanceEnabled, multiTenancy, multiInstitution, sysadminExtraEnabled, - enableGuestInvitation, enableTermsAndConditions, enableFileScan, enableWorkWeixinDepartments } from '../../utils/constants'; + enableGuestInvitation, enableTermsAndConditions, enableFileScan, enableWorkWeixin } from '../../utils/constants'; const propTypes = { isSidePanelClosed: PropTypes.bool.isRequired, @@ -174,7 +174,7 @@ class SidePanel extends React.Component { } - {isDefaultAdmin && enableWorkWeixinDepartments && + {isDefaultAdmin && enableWorkWeixin &&
  • diff --git a/frontend/src/pages/wiki/main-panel.js b/frontend/src/pages/wiki/main-panel.js index d53a83c9db..2e66391356 100644 --- a/frontend/src/pages/wiki/main-panel.js +++ b/frontend/src/pages/wiki/main-panel.js @@ -7,6 +7,7 @@ import WikiMarkdownViewer from '../../components/wiki-markdown-viewer'; import WikiDirListView from '../../components/wiki-dir-list-view/wiki-dir-list-view'; import Loading from '../../components/loading'; import { Utils } from '../../utils/utils'; +import Search from '../../components/search/search'; const propTypes = { path: PropTypes.string.isRequired, @@ -77,6 +78,21 @@ class MainPanel extends Component { return (
    + {!username && + +
    + +
    +
    + +
    +
    + } {username && (
    diff --git a/frontend/src/utils/constants.js b/frontend/src/utils/constants.js index 6dfcf9fc76..9540149924 100644 --- a/frontend/src/utils/constants.js +++ b/frontend/src/utils/constants.js @@ -119,5 +119,5 @@ export const canManageUser = window.sysadmin ? window.sysadmin.pageOptions.admin export const canManageGroup = window.sysadmin ? window.sysadmin.pageOptions.admin_permissions.can_manage_group : ''; export const canViewUserLog = window.sysadmin ? window.sysadmin.pageOptions.admin_permissions.can_view_user_log : ''; export const canViewAdminLog = window.sysadmin ? window.sysadmin.pageOptions.admin_permissions.can_view_admin_log : ''; -export const enableWorkWeixinDepartments = window.sysadmin ? window.sysadmin.pageOptions.enable_work_weixin_departments : ''; +export const enableWorkWeixin = window.sysadmin ? window.sysadmin.pageOptions.enable_work_weixin : ''; diff --git a/frontend/src/utils/utils.js b/frontend/src/utils/utils.js index 1fc09db32b..c672780604 100644 --- a/frontend/src/utils/utils.js +++ b/frontend/src/utils/utils.js @@ -240,7 +240,8 @@ export const Utils = { isSupportUploadFolder: function() { return navigator.userAgent.indexOf('Firefox')!=-1 || - navigator.userAgent.indexOf('Chrome') > -1; + navigator.userAgent.indexOf('Chrome') > -1 || + navigator.userAgent.indexOf("Safari") > -1; }, isIEBrower: function() { // is ie <= ie11 not include Edge diff --git a/seahub/api2/endpoints/public_repos_search.py b/seahub/api2/endpoints/public_repos_search.py new file mode 100644 index 0000000000..bdb0d7d4de --- /dev/null +++ b/seahub/api2/endpoints/public_repos_search.py @@ -0,0 +1,102 @@ +# -*- coding: utf-8 -*- +import logging + +from rest_framework.views import APIView +from rest_framework.authentication import SessionAuthentication +from rest_framework.permissions import IsAuthenticatedOrReadOnly +from rest_framework.response import Response +from rest_framework import status +from seaserv import seafile_api + +from seahub.api2.authentication import TokenAuthentication +from seahub.api2.throttling import UserRateThrottle +from seahub.api2.utils import api_error +from seahub.utils.repo import is_valid_repo_id_format +from seahub.utils import HAS_FILE_SEARCH +from seahub.wiki.models import Wiki +if HAS_FILE_SEARCH: + from seahub_extra.search.utils import search_files + + +logger = logging.getLogger('seafes') + + +class PublishedRepoSearchView(APIView): + """ Search public repos + """ + authentication_classes = (TokenAuthentication, SessionAuthentication) + permission_classes = (IsAuthenticatedOrReadOnly,) + throttle_classes = (UserRateThrottle, ) + + def get(self, request): + # is search supported + if not HAS_FILE_SEARCH: + error_msg = 'Search not supported.' + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + # argument check + keyword = request.GET.get('q', None) + if not keyword: + error_msg = 'q invalid.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + repo_id = request.GET.get('repo_id', None) + if not is_valid_repo_id_format(repo_id): + error_msg = 'repo_id invalid.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + # recourse 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) + + # permission check + wiki = Wiki.objects.filter(repo_id=repo_id)[0] + if not wiki.has_read_perm(request): + error_msg = 'Permission denied.' + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + + try: + current_page = int(request.GET.get('page', '1')) + per_page = int(request.GET.get('per_page', '10')) + if per_page > 100: + per_page = 100 + except ValueError: + current_page = 1 + per_page = 10 + + start = (current_page - 1) * per_page + size = per_page + if start < 0 or size < 0: + error_msg = 'page or per_page invalid.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + repo_id_map = {} + map_id = repo.origin_repo_id if repo.origin_repo_id else repo_id + repo_id_map[map_id] = repo + # search file + try: + results, total = search_files( + repo_id_map, None, keyword, None, start, size, org_id=None + ) + except Exception as e: + logger.error(e) + error_msg = 'Internal Server Error' + return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) + + for result in results: + result.pop('repo', None) + result.pop('exists', None) + result.pop('last_modified_by', None) + result.pop('name_highlight', None) + result.pop('score', None) + result['repo_type'] = 'public' + + has_more = True if total > current_page * per_page else False + + return Response({ + "total": total, + "results": results, + "has_more": has_more + }) diff --git a/seahub/api2/endpoints/upload_links.py b/seahub/api2/endpoints/upload_links.py index 03777cc832..290b0ebff7 100644 --- a/seahub/api2/endpoints/upload_links.py +++ b/seahub/api2/endpoints/upload_links.py @@ -2,6 +2,7 @@ import os import logging from constance import config +from dateutil.relativedelta import relativedelta from rest_framework.authentication import SessionAuthentication from rest_framework.permissions import IsAuthenticated @@ -9,6 +10,7 @@ from rest_framework.response import Response from rest_framework.views import APIView from rest_framework import status +from django.utils import timezone from django.utils.translation import ugettext as _ from seaserv import seafile_api @@ -48,6 +50,11 @@ def get_upload_link_info(uls): else: ctime = '' + if uls.expire_date: + expire_date = datetime_to_isoformat_timestr(uls.expire_date) + else: + expire_date = '' + data['repo_id'] = repo_id data['repo_name'] = repo.repo_name if repo else '' data['path'] = path @@ -57,6 +64,8 @@ def get_upload_link_info(uls): data['link'] = gen_shared_upload_link(token) data['token'] = token data['username'] = uls.username + data['expire_date'] = expire_date + data['is_expired'] = uls.is_expired() return data @@ -143,6 +152,12 @@ class UploadLinks(APIView): error_msg = _('Password is too short') return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + try: + expire_days = int(request.data.get('expire_days', 0)) + except ValueError: + error_msg = 'expire_days invalid.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + # resource check repo = seafile_api.get_repo(repo_id) if not repo: @@ -164,11 +179,16 @@ class UploadLinks(APIView): error_msg = 'Permission denied.' return api_error(status.HTTP_403_FORBIDDEN, error_msg) + if expire_days <= 0: + expire_date = None + else: + expire_date = timezone.now() + relativedelta(days=expire_days) + username = request.user.username uls = UploadLinkShare.objects.get_upload_link_by_path(username, repo_id, path) if not uls: uls = UploadLinkShare.objects.create_upload_link_share(username, - repo_id, path, password) + repo_id, path, password, expire_date) link_info = get_upload_link_info(uls) return Response(link_info) diff --git a/seahub/auth/views.py b/seahub/auth/views.py index 756c5aff6a..5b3efb70d3 100644 --- a/seahub/auth/views.py +++ b/seahub/auth/views.py @@ -186,7 +186,7 @@ def login(request, template_name='registration/login.html', getattr(settings, 'ENABLE_OAUTH', False) or \ getattr(settings, 'ENABLE_CAS', False) or \ getattr(settings, 'ENABLE_REMOTE_USER_AUTHENTICATION', False) or \ - getattr(settings, 'ENABLE_WORK_WEIXIN_OAUTH', False) + getattr(settings, 'ENABLE_WORK_WEIXIN', False) login_bg_image_path = get_login_bg_image_path() diff --git a/seahub/base/context_processors.py b/seahub/base/context_processors.py index d7c5a035ee..1c16b55ca9 100644 --- a/seahub/base/context_processors.py +++ b/seahub/base/context_processors.py @@ -50,7 +50,7 @@ try: from seahub.settings import ENABLE_FILE_SCAN except ImportError: ENABLE_FILE_SCAN = False -from seahub.work_weixin.settings import ENABLE_WORK_WEIXIN_DEPARTMENTS +from seahub.work_weixin.settings import ENABLE_WORK_WEIXIN def base(request): @@ -134,7 +134,7 @@ def base(request): 'enable_resumable_fileupload': dj_settings.ENABLE_RESUMABLE_FILEUPLOAD, 'service_url': get_service_url().rstrip('/'), 'enable_file_scan': ENABLE_FILE_SCAN, - 'enable_work_weixin_departments': ENABLE_WORK_WEIXIN_DEPARTMENTS, + 'enable_work_weixin': ENABLE_WORK_WEIXIN, 'avatar_url': avatar_url if avatar_url else '', } diff --git a/seahub/oauth/backends.py b/seahub/oauth/backends.py index 6cad124101..4f826dc0d0 100644 --- a/seahub/oauth/backends.py +++ b/seahub/oauth/backends.py @@ -4,7 +4,7 @@ from seahub.auth.backends import RemoteUserBackend from seahub.base.accounts import User from registration.models import (notify_admins_on_activate_request, notify_admins_on_register_complete) -from seahub.work_weixin.settings import ENABLE_WORK_WEIXIN_OAUTH +from seahub.work_weixin.settings import ENABLE_WORK_WEIXIN class OauthRemoteUserBackend(RemoteUserBackend): """ @@ -23,7 +23,7 @@ class OauthRemoteUserBackend(RemoteUserBackend): # Create active user by default. activate_after_creation = getattr(settings, 'OAUTH_ACTIVATE_USER_AFTER_CREATION', True) - if ENABLE_WORK_WEIXIN_OAUTH: + if ENABLE_WORK_WEIXIN: create_unknown_user = getattr(settings, 'WORK_WEIXIN_OAUTH_CREATE_UNKNOWN_USER', True) activate_after_creation = getattr(settings, 'WORK_WEIXIN_OAUTH_ACTIVATE_USER_AFTER_CREATION', True) diff --git a/seahub/settings.py b/seahub/settings.py index fa17564503..1542d0b257 100644 --- a/seahub/settings.py +++ b/seahub/settings.py @@ -303,12 +303,8 @@ SOCIAL_AUTH_PIPELINE = ( ENABLE_OAUTH = False ENABLE_WATERMARK = False -# allow user scan the work weixin qrcode to login -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 +# enable work weixin +ENABLE_WORK_WEIXIN = False # allow user to clean library trash ENABLE_USER_CLEAN_TRASH = True @@ -917,5 +913,5 @@ if ENABLE_REMOTE_USER_AUTHENTICATION: MIDDLEWARE_CLASSES += ('seahub.auth.middleware.SeafileRemoteUserMiddleware',) AUTHENTICATION_BACKENDS += ('seahub.auth.backends.SeafileRemoteUserBackend',) -if ENABLE_OAUTH or ENABLE_WORK_WEIXIN_OAUTH: +if ENABLE_OAUTH or ENABLE_WORK_WEIXIN: AUTHENTICATION_BACKENDS += ('seahub.oauth.backends.OauthRemoteUserBackend',) diff --git a/seahub/share/models.py b/seahub/share/models.py index 408ab90772..6291cc5683 100644 --- a/seahub/share/models.py +++ b/seahub/share/models.py @@ -451,6 +451,12 @@ class UploadLinkShare(models.Model): def is_owner(self, owner): return owner == self.username + def is_expired(self): + if self.expire_date is not None and timezone.now() > self.expire_date: + return True + else: + return False + class PrivateFileDirShareManager(models.Manager): def add_private_file_share(self, from_user, to_user, repo_id, path, perm): """ diff --git a/seahub/templates/js/sysadmin-templates.html b/seahub/templates/js/sysadmin-templates.html index 26eaaaaa54..a3c8739120 100644 --- a/seahub/templates/js/sysadmin-templates.html +++ b/seahub/templates/js/sysadmin-templates.html @@ -122,7 +122,7 @@
  • {% endif %} - {% if is_default_admin and enable_work_weixin_departments %} + {% if is_default_admin and enable_work_weixin %}
  • 企业微信集成
  • diff --git a/seahub/templates/sysadmin/base.html b/seahub/templates/sysadmin/base.html index 6a1dc78941..688b59f599 100644 --- a/seahub/templates/sysadmin/base.html +++ b/seahub/templates/sysadmin/base.html @@ -133,7 +133,7 @@ {% endif %} - {% if is_default_admin and enable_work_weixin_departments %} + {% if is_default_admin and enable_work_weixin %}
  • 企业微信集成
  • diff --git a/seahub/templates/sysadmin/sysadmin_backbone.html b/seahub/templates/sysadmin/sysadmin_backbone.html index c584b86834..064bbf2acf 100644 --- a/seahub/templates/sysadmin/sysadmin_backbone.html +++ b/seahub/templates/sysadmin/sysadmin_backbone.html @@ -108,7 +108,7 @@ app["pageOptions"] = { cur_note: {% if request.cur_note %} {'id': '{{ request.cur_note.id }}'} {% else %} null {% endif %}, is_default_admin: {% if is_default_admin %} true {% else %} false {% endif %}, enable_file_scan: {% if enable_file_scan %} true {% else %} false {% endif %}, - enable_work_weixin_departments: {% if enable_work_weixin_departments %} true {% else %} false {% endif %}, + enable_work_weixin: {% if enable_work_weixin %} true {% else %} false {% endif %}, admin_permissions: { "can_view_system_info": {% if user.admin_permissions.can_view_system_info %} true {% else %} false {% endif %}, "can_view_statistic": {% if user.admin_permissions.can_view_statistic %} true {% else %} false {% endif %}, diff --git a/seahub/templates/sysadmin/sysadmin_react_app.html b/seahub/templates/sysadmin/sysadmin_react_app.html index 7b7ae200ca..bc1a5eb039 100644 --- a/seahub/templates/sysadmin/sysadmin_react_app.html +++ b/seahub/templates/sysadmin/sysadmin_react_app.html @@ -15,7 +15,7 @@ enable_terms_and_conditions: {% if enable_terms_and_conditions %} true {% else %} false {% endif %}, is_default_admin: {% if is_default_admin %} true {% else %} false {% endif %}, enable_file_scan: {% if enable_file_scan %} true {% else %} false {% endif %}, - enable_work_weixin_departments: {% if enable_work_weixin_departments %} true {% else %} false {% endif %}, + enable_work_weixin: {% if enable_work_weixin %} true {% else %} false {% endif %}, admin_permissions: { "can_view_system_info": {% if user.admin_permissions.can_view_system_info %} true {% else %} false {% endif %}, "can_view_statistic": {% if user.admin_permissions.can_view_statistic %} true {% else %} false {% endif %}, diff --git a/seahub/urls.py b/seahub/urls.py index 1f48efa364..54d8a56253 100644 --- a/seahub/urls.py +++ b/seahub/urls.py @@ -89,10 +89,12 @@ from seahub.api2.endpoints.related_files import RelatedFilesView, RelatedFileVie from seahub.api2.endpoints.webdav_secret import WebdavSecretView from seahub.api2.endpoints.starred_items import StarredItems from seahub.api2.endpoints.markdown_lint import MarkdownLintView +from seahub.api2.endpoints.public_repos_search import PublishedRepoSearchView from seahub.api2.endpoints.dtable import WorkspacesView, DTableView, DTablesView, \ DTableUpdateLinkView, DTableAssetUploadLinkView, dtable_file_view, dtable_asset_access from seahub.api2.endpoints.dtable_share import SharedDTablesView, DTableShareView + # Admin from seahub.api2.endpoints.admin.revision_tag import AdminTaggedItemsView from seahub.api2.endpoints.admin.login_logs import LoginLogs, AdminLoginLogs @@ -366,6 +368,9 @@ urlpatterns = [ # user: markdown-lint url(r'^api/v2.1/markdown-lint/$', MarkdownLintView.as_view(), name='api-v2.1-markdown-lint'), + # public repos search + url(r'^api/v2.1/published-repo-search/$', PublishedRepoSearchView.as_view(), name='api-v2.1-published-repo-search'), + # user: workspaces url(r'^api/v2.1/workspaces/$', WorkspacesView.as_view(), name='api-v2.1-workspaces'), url(r'^api/v2.1/dtables/$', DTablesView.as_view(), name='api-v2.1-dtables'), diff --git a/seahub/views/sso.py b/seahub/views/sso.py index 22ee8471e9..28d63a3e92 100644 --- a/seahub/views/sso.py +++ b/seahub/views/sso.py @@ -35,7 +35,7 @@ def sso(request): if getattr(settings, 'ENABLE_CAS', False): return HttpResponseRedirect(reverse('cas_ng_login') + next_param) - if getattr(settings, 'ENABLE_WORK_WEIXIN_OAUTH', False): + if getattr(settings, 'ENABLE_WORK_WEIXIN', False): return HttpResponseRedirect(reverse('work_weixin_oauth_login') + next_param) return HttpResponseRedirect(next_page) diff --git a/seahub/views/sysadmin.py b/seahub/views/sysadmin.py index c7a07d8b33..19274811e8 100644 --- a/seahub/views/sysadmin.py +++ b/seahub/views/sysadmin.py @@ -98,7 +98,7 @@ try: from seahub.settings import ENABLE_FILE_SCAN except ImportError: ENABLE_FILE_SCAN = False -from seahub.work_weixin.settings import ENABLE_WORK_WEIXIN_DEPARTMENTS +from seahub.work_weixin.settings import ENABLE_WORK_WEIXIN logger = logging.getLogger(__name__) @@ -133,7 +133,7 @@ def sysadmin(request): 'enable_limit_ipaddress': ENABLE_LIMIT_IPADDRESS, 'trash_repos_expire_days': expire_days if expire_days > 0 else 30, 'enable_file_scan': ENABLE_FILE_SCAN, - 'enable_work_weixin_departments': ENABLE_WORK_WEIXIN_DEPARTMENTS, + 'enable_work_weixin': ENABLE_WORK_WEIXIN, }) @login_required @@ -148,7 +148,7 @@ def sysadmin_react_fake_view(request): 'enable_guest_invitation': ENABLE_GUEST_INVITATION, 'enable_terms_and_conditions': config.ENABLE_TERMS_AND_CONDITIONS, 'enable_file_scan': ENABLE_FILE_SCAN, - 'enable_work_weixin_departments': ENABLE_WORK_WEIXIN_DEPARTMENTS, + 'enable_work_weixin': ENABLE_WORK_WEIXIN, }) @login_required diff --git a/seahub/work_weixin/settings.py b/seahub/work_weixin/settings.py index f3623a8e3a..c3e36a1d01 100644 --- a/seahub/work_weixin/settings.py +++ b/seahub/work_weixin/settings.py @@ -3,13 +3,13 @@ from django.conf import settings # # work weixin base +ENABLE_WORK_WEIXIN = getattr(settings, 'ENABLE_WORK_WEIXIN', False) WORK_WEIXIN_CORP_ID = getattr(settings, 'WORK_WEIXIN_CORP_ID', '') WORK_WEIXIN_AGENT_SECRET = getattr(settings, 'WORK_WEIXIN_AGENT_SECRET', '') WORK_WEIXIN_ACCESS_TOKEN_URL = getattr(settings, 'WORK_WEIXIN_ACCESS_TOKEN_URL', 'https://qyapi.weixin.qq.com/cgi-bin/gettoken') # # admin work weixin departments -ENABLE_WORK_WEIXIN_DEPARTMENTS = getattr(settings, 'ENABLE_WORK_WEIXIN_DEPARTMENTS', False) WORK_WEIXIN_DEPARTMENTS_URL = getattr(settings, 'WORK_WEIXIN_DEPARTMENTS_URL', 'https://qyapi.weixin.qq.com/cgi-bin/department/list') WORK_WEIXIN_DEPARTMENT_MEMBERS_URL = getattr(settings, 'WORK_WEIXIN_DEPARTMENT_MEMBERS_URL', @@ -17,7 +17,6 @@ WORK_WEIXIN_DEPARTMENT_MEMBERS_URL = getattr(settings, 'WORK_WEIXIN_DEPARTMENT_M # # work weixin oauth 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 + '_' 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', @@ -28,7 +27,6 @@ WORK_WEIXIN_GET_USER_PROFILE_URL = getattr(settings, 'WORK_WEIXIN_GET_USER_PROFI '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') diff --git a/seahub/work_weixin/utils.py b/seahub/work_weixin/utils.py index feb27c9258..0904cdfb1c 100644 --- a/seahub/work_weixin/utils.py +++ b/seahub/work_weixin/utils.py @@ -8,11 +8,11 @@ import requests from django.core.cache import cache from seahub.utils import normalize_cache_key from seahub.work_weixin.settings import WORK_WEIXIN_CORP_ID, WORK_WEIXIN_AGENT_SECRET, \ - WORK_WEIXIN_ACCESS_TOKEN_URL, ENABLE_WORK_WEIXIN_DEPARTMENTS, \ + WORK_WEIXIN_ACCESS_TOKEN_URL, ENABLE_WORK_WEIXIN, \ WORK_WEIXIN_DEPARTMENTS_URL, WORK_WEIXIN_DEPARTMENT_MEMBERS_URL, \ - ENABLE_WORK_WEIXIN_OAUTH, WORK_WEIXIN_AGENT_ID, WORK_WEIXIN_AUTHORIZATION_URL, \ + WORK_WEIXIN_AGENT_ID, WORK_WEIXIN_AUTHORIZATION_URL, \ WORK_WEIXIN_GET_USER_INFO_URL, WORK_WEIXIN_GET_USER_PROFILE_URL, \ - ENABLE_WORK_WEIXIN_NOTIFICATIONS, WORK_WEIXIN_NOTIFICATIONS_URL + WORK_WEIXIN_NOTIFICATIONS_URL from seahub.profile.models import Profile logger = logging.getLogger(__name__) @@ -66,34 +66,35 @@ def handler_work_weixin_api_response(response): def work_weixin_base_check(): """ work weixin base check """ + if not ENABLE_WORK_WEIXIN: + return False + 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) logger.error('WORK_WEIXIN_AGENT_SECRET: %s' % WORK_WEIXIN_AGENT_SECRET) logger.error('WORK_WEIXIN_ACCESS_TOKEN_URL: %s' % WORK_WEIXIN_ACCESS_TOKEN_URL) return False + return True def work_weixin_oauth_check(): """ use for work weixin login and profile bind """ - if not ENABLE_WORK_WEIXIN_OAUTH: + if not work_weixin_base_check(): 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 \ - or not WORK_WEIXIN_GET_USER_PROFILE_URL: - logger.error('work weixin oauth relevant settings invalid.') - logger.error('WORK_WEIXIN_AGENT_ID: %s' % WORK_WEIXIN_AGENT_ID) - logger.error('WORK_WEIXIN_GET_USER_INFO_URL: %s' % WORK_WEIXIN_GET_USER_INFO_URL) - logger.error('WORK_WEIXIN_AUTHORIZATION_URL: %s' % WORK_WEIXIN_AUTHORIZATION_URL) - logger.error('WORK_WEIXIN_GET_USER_PROFILE_URL: %s' % WORK_WEIXIN_GET_USER_PROFILE_URL) - return False + if not WORK_WEIXIN_AGENT_ID \ + or not WORK_WEIXIN_GET_USER_INFO_URL \ + or not WORK_WEIXIN_AUTHORIZATION_URL \ + or not WORK_WEIXIN_GET_USER_PROFILE_URL: + logger.error('work weixin oauth relevant settings invalid.') + logger.error('WORK_WEIXIN_AGENT_ID: %s' % WORK_WEIXIN_AGENT_ID) + logger.error('WORK_WEIXIN_GET_USER_INFO_URL: %s' % WORK_WEIXIN_GET_USER_INFO_URL) + logger.error('WORK_WEIXIN_AUTHORIZATION_URL: %s' % WORK_WEIXIN_AUTHORIZATION_URL) + logger.error('WORK_WEIXIN_GET_USER_PROFILE_URL: %s' % WORK_WEIXIN_GET_USER_PROFILE_URL) + return False return True @@ -101,18 +102,15 @@ def work_weixin_oauth_check(): def admin_work_weixin_departments_check(): """ use for admin work weixin departments """ - if not ENABLE_WORK_WEIXIN_DEPARTMENTS: + if not work_weixin_base_check(): 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.') - logger.error('WORK_WEIXIN_DEPARTMENTS_URL: %s' % WORK_WEIXIN_DEPARTMENTS_URL) - logger.error('WORK_WEIXIN_DEPARTMENT_MEMBERS_URL: %s' % WORK_WEIXIN_DEPARTMENT_MEMBERS_URL) - return False + if not WORK_WEIXIN_DEPARTMENTS_URL \ + or not WORK_WEIXIN_DEPARTMENT_MEMBERS_URL: + logger.error('admin work weixin departments relevant settings invalid.') + logger.error('WORK_WEIXIN_DEPARTMENTS_URL: %s' % WORK_WEIXIN_DEPARTMENTS_URL) + logger.error('WORK_WEIXIN_DEPARTMENT_MEMBERS_URL: %s' % WORK_WEIXIN_DEPARTMENT_MEMBERS_URL) + return False return True @@ -120,18 +118,15 @@ def admin_work_weixin_departments_check(): def work_weixin_notifications_check(): """ use for send work weixin notifications """ - if not ENABLE_WORK_WEIXIN_NOTIFICATIONS: + if not work_weixin_base_check(): 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 + 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 diff --git a/tests/api/endpoints/test_upload_links.py b/tests/api/endpoints/test_upload_links.py index 98534e9c59..e2337830f3 100644 --- a/tests/api/endpoints/test_upload_links.py +++ b/tests/api/endpoints/test_upload_links.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- import json from mock import patch +from dateutil.relativedelta import relativedelta +from django.utils import timezone from django.core.urlresolvers import reverse @@ -29,9 +31,9 @@ class UploadLinksTest(BaseTestCase): def tearDown(self): self.remove_repo() - def _add_upload_link(self): + def _add_upload_link(self, expire_date=None): upload_link = UploadLinkShare.objects.create_upload_link_share(self.user_name, - self.repo_id, self.folder_path, None, None) + self.repo_id, self.folder_path, None, expire_date=expire_date) return upload_link.token @@ -50,6 +52,8 @@ class UploadLinksTest(BaseTestCase): assert json_resp[0]['link'] is not None assert json_resp[0]['token'] is not None + assert json_resp[0]['is_expired'] is not None + assert token in json_resp[0]['link'] assert 'u/d' in json_resp[0]['link'] @@ -58,6 +62,20 @@ class UploadLinksTest(BaseTestCase): self._remove_upload_link(token) + def test_get_expired_upload_link(self): + self.login_as(self.user) + # create a upload link expired one day ago. + expire_date = timezone.now() + relativedelta(days=-1) + token = self._add_upload_link(expire_date=expire_date) + + resp = self.client.get(self.url + '?path=' + self.folder_path + '&repo_id=' + self.repo_id) + self.assertEqual(200, resp.status_code) + + json_resp = json.loads(resp.content) + assert json_resp[0]['is_expired'] == True + + self._remove_upload_link(token) + @patch.object(CanGenerateUploadLink, 'has_permission') def test_get_link_with_invalid_user_role_permission(self, mock_has_permission): self.login_as(self.user)