diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 0aa347bbca..822ca55624 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -12022,9 +12022,9 @@ } }, "seafile-js": { - "version": "0.2.154", - "resolved": "https://registry.npmjs.org/seafile-js/-/seafile-js-0.2.154.tgz", - "integrity": "sha512-8Vy+RIK4P7yZowr/smgd/6eLOOpT4pq4UbJXhjE0XDp6s6WERmLr1nZUMA1QjMERMlfJ3ymq2Jk30rLsN82Lrg==", + "version": "0.2.156", + "resolved": "https://registry.npmjs.org/seafile-js/-/seafile-js-0.2.156.tgz", + "integrity": "sha512-D8ZUwNNay8WTFqIMF6AzysKFFkzFAdGgGapHsvpijOl2lETg9Pg+OlWawlFzDxaTKu4JxSTk2g9On/RGvqswpQ==", "requires": { "axios": "^0.18.0", "form-data": "^2.3.2", diff --git a/frontend/package.json b/frontend/package.json index 985b5be0e1..0a8cec052a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -44,7 +44,7 @@ "react-responsive": "^6.1.2", "react-select": "^2.4.1", "reactstrap": "^6.4.0", - "seafile-js": "^0.2.154", + "seafile-js": "0.2.156", "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-unlink-device.js b/frontend/src/components/dialog/confirm-unlink-device.js new file mode 100644 index 0000000000..c38e52a4f2 --- /dev/null +++ b/frontend/src/components/dialog/confirm-unlink-device.js @@ -0,0 +1,59 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { Modal, ModalHeader, ModalBody, ModalFooter, Button, FormGroup, Label, Input } from 'reactstrap'; +import { gettext } from '../../utils/constants'; + +const propTypes = { + executeOperation: PropTypes.func.isRequired, + toggleDialog: PropTypes.func.isRequired +}; + +class ConfirmUnlinkDevice extends Component { + + constructor(props) { + super(props); + this.state = { + isChecked: false + }; + } + + toggle = () => { + this.props.toggleDialog(); + } + + executeOperation = () => { + this.toggle(); + this.props.executeOperation(this.state.isChecked); + } + + onInputChange = (e) => { + this.setState({ + isChecked: e.target.checked + }); + } + + render() { + return ( + <Modal isOpen={true} toggle={this.toggle}> + <ModalHeader toggle={this.toggle}>{gettext('Unlink device')}</ModalHeader> + <ModalBody> + <p>{gettext('Are you sure you want to unlink this device?')}</p> + <FormGroup check> + <Label check> + <Input type="checkbox" checked={this.state.isChecked} onChange={this.onInputChange} /> + <span>{gettext('Delete files from this device the next time it comes online.')}</span> + </Label> + </FormGroup> + </ModalBody> + <ModalFooter> + <Button color="secondary" onClick={this.toggle}>{gettext('Cancel')}</Button> + <Button color="primary" onClick={this.executeOperation}>{gettext('Unlink')}</Button> + </ModalFooter> + </Modal> + ); + } +} + +ConfirmUnlinkDevice.propTypes = propTypes; + +export default ConfirmUnlinkDevice; diff --git a/frontend/src/components/dialog/org-add-user-dialog.js b/frontend/src/components/dialog/org-add-user-dialog.js index bed6bdcc95..d1940b2b34 100644 --- a/frontend/src/components/dialog/org-add-user-dialog.js +++ b/frontend/src/components/dialog/org-add-user-dialog.js @@ -30,7 +30,7 @@ class AddOrgUserDialog extends React.Component { if (isValid) { let { email, name, password } = this.state; this.setState({isAddingUser: true}); - this.props.handleSubmit(email, name, password); + this.props.handleSubmit(email, name.trim(), password); } } @@ -71,7 +71,7 @@ class AddOrgUserDialog extends React.Component { } inputName = (e) => { - let name = e.target.value.trim(); + let name = e.target.value; this.setState({name: name}); } @@ -107,7 +107,7 @@ class AddOrgUserDialog extends React.Component { this.setState({errMessage: errMessage}); return false; } - let name = this.state.name; + let name = this.state.name.trim(); if (!name.length) { errMessage = gettext('Name is required'); this.setState({errMessage: errMessage}); diff --git a/frontend/src/components/shared-repo-list-view/shared-repo-list-item.js b/frontend/src/components/shared-repo-list-view/shared-repo-list-item.js index 7aed0570e0..4b43cf1b63 100644 --- a/frontend/src/components/shared-repo-list-view/shared-repo-list-item.js +++ b/frontend/src/components/shared-repo-list-view/shared-repo-list-item.js @@ -4,11 +4,13 @@ import moment from 'moment'; import { Dropdown, DropdownMenu, DropdownToggle, DropdownItem } from 'reactstrap'; import { Link } from '@reach/router'; import { Utils } from '../../utils/utils'; -import { gettext, siteRoot, isPro, username, folderPermEnabled, isSystemStaff } from '../../utils/constants'; +import { gettext, siteRoot, isPro, username, folderPermEnabled, isSystemStaff, enableResetEncryptedRepoPassword, isEmailConfigured } from '../../utils/constants'; import ModalPortal from '../../components/modal-portal'; import ShareDialog from '../../components/dialog/share-dialog'; import LibSubFolderPermissionDialog from '../../components/dialog/lib-sub-folder-permission-dialog'; import DeleteRepoDialog from '../../components/dialog/delete-repo-dialog'; +import ChangeRepoPasswordDialog from '../../components/dialog/change-repo-password-dialog'; +import ResetEncryptedRepoPasswordDialog from '../../components/dialog/reset-encrypted-repo-password-dialog'; import Rename from '../rename'; import { seafileAPI } from '../../utils/seafile-api'; import LibHistorySettingDialog from '../dialog/lib-history-setting-dialog'; @@ -46,6 +48,8 @@ class SharedRepoListItem extends React.Component { isAPITokenDialogShow: false, isRepoShareUploadLinksDialogOpen: false, isRepoDeleted: false, + isChangePasswordDialogShow: false, + isResetPasswordDialogShow: false }; this.isDeparementOnwerGroupMember = false; } @@ -141,6 +145,12 @@ class SharedRepoListItem extends React.Component { case 'Share Links Admin': this.toggleRepoShareUploadLinksDialog(); break; + case 'Change Password': + this.onChangePasswordToggle(); + break; + case 'Reset Password': + this.onResetPasswordToggle(); + break; default: break; } @@ -231,6 +241,14 @@ class SharedRepoListItem extends React.Component { this.setState({isAPITokenDialogShow: !this.state.isAPITokenDialogShow}); } + onChangePasswordToggle = () => { + this.setState({isChangePasswordDialogShow: !this.state.isChangePasswordDialogShow}); + } + + onResetPasswordToggle = () => { + this.setState({isResetPasswordDialogShow: !this.state.isResetPasswordDialogShow}); + } + translateMenuItem = (menuItem) => { let translateResult = ''; switch(menuItem) { @@ -255,6 +273,12 @@ class SharedRepoListItem extends React.Component { case 'Share Links Admin': translateResult = gettext('Share Links Admin'); break; + case 'Change Password': + translateResult = gettext('Change Password'); + break; + case 'Reset Password': + translateResult = gettext('Reset Password'); + break; case 'API Token': translateResult = 'API Token'; // translation is not needed here break; @@ -280,7 +304,14 @@ class SharedRepoListItem extends React.Component { if (folderPermEnabled) { operations.push('Folder Permission'); } - operations.push('Share Links Admin', 'History Setting', 'API Token', 'Details'); + operations.push('Share Links Admin'); + if (repo.encrypted) { + operations.push('Change Password'); + } + if (repo.encrypted && enableResetEncryptedRepoPassword && isEmailConfigured) { + operations.push('Reset Password'); + } + operations.push('History Setting', 'API Token', 'Details'); } else { operations.push('Unshare'); } @@ -538,6 +569,23 @@ class SharedRepoListItem extends React.Component { /> </ModalPortal> )} + {this.state.isChangePasswordDialogShow && ( + <ModalPortal> + <ChangeRepoPasswordDialog + repoID={repo.repo_id} + repoName={repo.repo_name} + toggleDialog={this.onChangePasswordToggle} + /> + </ModalPortal> + )} + {this.state.isResetPasswordDialogShow && ( + <ModalPortal> + <ResetEncryptedRepoPasswordDialog + repoID={repo.repo_id} + toggleDialog={this.onResetPasswordToggle} + /> + </ModalPortal> + )} </Fragment> ); } diff --git a/frontend/src/models/org-user.js b/frontend/src/models/org-user.js index 5efcbff769..1a13045d98 100644 --- a/frontend/src/models/org-user.js +++ b/frontend/src/models/org-user.js @@ -11,8 +11,8 @@ class OrgUserInfo { this.email = object.email; this.contact_email = object.owner_contact_email; this.is_active = object.is_active; - this.quota = object.quota > 0 ? Utils.bytesToSize(object.quota) : ''; - this.self_usage = Utils.bytesToSize(object.self_usage); + this.quota_usage = object.quota_usage; + this.quota_total = object.quota_total; this.last_login = object.last_login ? moment(object.last_login).fromNow() : '--'; this.ctime = moment(object.ctime).format('YYYY-MM-DD HH:mm:ss'); } diff --git a/frontend/src/pages/linked-devices/linked-devices.js b/frontend/src/pages/linked-devices/linked-devices.js index 524ea01faa..70f3654809 100644 --- a/frontend/src/pages/linked-devices/linked-devices.js +++ b/frontend/src/pages/linked-devices/linked-devices.js @@ -5,6 +5,7 @@ import { seafileAPI } from '../../utils/seafile-api'; import { gettext } from '../../utils/constants'; import toaster from '../../components/toast'; import EmptyTip from '../../components/empty-tip'; +import ConfirmUnlinkDeviceDialog from '../../components/dialog/confirm-unlink-device'; import { Utils } from '../../utils/utils'; class Content extends Component { @@ -65,8 +66,9 @@ class Item extends Component { super(props); this.state = { isOpMenuOpen: false, // for mobile - showOpIcon: false, - unlinked: false + isOpIconShown: false, + unlinked: false, + isConfirmUnlinkDialogOpen: false }; } @@ -78,13 +80,19 @@ class Item extends Component { handleMouseOver = () => { this.setState({ - showOpIcon: true + isOpIconShown: true }); } handleMouseOut = () => { this.setState({ - showOpIcon: false + isOpIconShown: false + }); + } + + toggleDialog = () => { + this.setState({ + isConfirmUnlinkDialogOpen: !this.state.isConfirmUnlinkDialogOpen }); } @@ -92,14 +100,23 @@ class Item extends Component { e.preventDefault(); const data = this.props.data; + if (data.is_desktop_client) { + this.toggleDialog(); + } else { + const wipeDevice = true; + this.unlinkDevice(wipeDevice); + } + } - seafileAPI.unlinkDevice(data.platform, data.device_id).then((res) => { + unlinkDevice = (wipeDevice) => { + const data = this.props.data; + seafileAPI.unlinkDevice(data.platform, data.device_id, wipeDevice).then((res) => { this.setState({ unlinked: true }); - let msg_s = gettext('Successfully unlinked %(name)s.'); - msg_s = msg_s.replace('%(name)s', data.device_name); - toaster.success(msg_s); + let msg = gettext('Successfully unlinked %(name)s.'); + msg = msg.replace('%(name)s', data.device_name); + toaster.success(msg); }).catch((error) => { let errMessage = Utils.getErrorMsg(error); toaster.danger(errMessage); @@ -114,7 +131,7 @@ class Item extends Component { const data = this.props.data; let opClasses = 'sf2-icon-delete unlink-device action-icon'; - opClasses += this.state.showOpIcon ? '' : ' invisible'; + opClasses += this.state.isOpIconShown ? '' : ' invisible'; const desktopItem = ( <tr onMouseOver={this.handleMouseOver} onMouseOut={this.handleMouseOut}> @@ -156,7 +173,17 @@ class Item extends Component { </tr> ); - return this.props.isDesktop ? desktopItem : mobileItem; + return ( + <React.Fragment> + {this.props.isDesktop ? desktopItem : mobileItem} + {this.state.isConfirmUnlinkDialogOpen && + <ConfirmUnlinkDeviceDialog + executeOperation={this.unlinkDevice} + toggleDialog={this.toggleDialog} + /> + } + </React.Fragment> + ); } } diff --git a/frontend/src/pages/org-admin/org-admin-list.js b/frontend/src/pages/org-admin/org-admin-list.js index 838fe02693..68f71ca85f 100644 --- a/frontend/src/pages/org-admin/org-admin-list.js +++ b/frontend/src/pages/org-admin/org-admin-list.js @@ -53,7 +53,7 @@ class OrgAdminList extends React.Component { {orgAdminUsers.map(item => { return ( <UserItem - key={item.id} + key={item.index} user={item} currentTab="admins" isItemFreezed={this.state.isItemFreezed} diff --git a/frontend/src/pages/org-admin/org-user-item.js b/frontend/src/pages/org-admin/org-user-item.js index bd1ddfe85d..f4f43124bc 100644 --- a/frontend/src/pages/org-admin/org-user-item.js +++ b/frontend/src/pages/org-admin/org-user-item.js @@ -55,13 +55,13 @@ class UserItem extends React.Component { } toggleResetPW = () => { - const email = this.props.user.email; + const { email, name } = this.props.user; toaster.success(gettext('Resetting user\'s password, please wait for a moment.')); seafileAPI.orgAdminResetOrgUserPassword(orgID, email).then(res => { let msg; msg = gettext('Successfully reset password to %(passwd)s for user %(user)s.'); msg = msg.replace('%(passwd)s', res.data.new_password); - msg = msg.replace('%(user)s', email); + msg = msg.replace('%(user)s', name); toaster.success(msg, { duration: 15 }); @@ -122,6 +122,17 @@ class UserItem extends React.Component { ); } + getQuotaTotal = (data) => { + switch (data) { + case -1: // failed to fetch quota + return gettext('Failed'); + case -2: + return '--'; + default: // data > 0 + return Utils.formatSize({bytes: data}); + } + } + render() { let { user, currentTab } = this.props; let href = siteRoot + 'org/useradmin/info/' + encodeURIComponent(user.email) + '/'; @@ -141,7 +152,7 @@ class UserItem extends React.Component { onStatusChanged={this.changeStatus} /> </td> - <td>{`${user.self_usage} / ${user.quota || '--'}`}</td> + <td>{`${Utils.formatSize({bytes: user.quota_usage})} / ${this.getQuotaTotal(user.quota_total)}`}</td> <td> {user.ctime} / <br /> diff --git a/frontend/src/pages/org-admin/org-users-list.js b/frontend/src/pages/org-admin/org-users-list.js index b63717442a..a564242627 100644 --- a/frontend/src/pages/org-admin/org-users-list.js +++ b/frontend/src/pages/org-admin/org-users-list.js @@ -71,10 +71,10 @@ class OrgUsersList extends React.Component { </tr> </thead> <tbody> - {orgUsers.map(item => { + {orgUsers.map((item, index) => { return ( <UserItem - key={item.id} + key={index} user={item} currentTab="users" isItemFreezed={this.state.isItemFreezed} diff --git a/frontend/src/pages/sys-admin/logs-page/share-permission-logs.js b/frontend/src/pages/sys-admin/logs-page/share-permission-logs.js index b283a6c546..4ca4831d78 100644 --- a/frontend/src/pages/sys-admin/logs-page/share-permission-logs.js +++ b/frontend/src/pages/sys-admin/logs-page/share-permission-logs.js @@ -1,17 +1,18 @@ import React, { Component, Fragment } from 'react'; +import { Link } from '@reach/router'; +import moment from 'moment'; +import { Button } from 'reactstrap'; import { seafileAPI } from '../../../utils/seafile-api'; import { gettext, siteRoot } from '../../../utils/constants'; import { Utils } from '../../../utils/utils'; -import EmptyTip from '../../../components/empty-tip'; -import moment from 'moment'; -import { Button } from 'reactstrap'; -import Loading from '../../../components/loading'; -import Paginator from '../../../components/paginator'; -import LogsNav from './logs-nav'; -import MainPanelTopbar from '../main-panel-topbar'; -import UserLink from '../user-link'; import LogsExportExcelDialog from '../../../components/dialog/sysadmin-dialog/sysadmin-logs-export-excel-dialog'; import ModalPortal from '../../../components/modal-portal'; +import EmptyTip from '../../../components/empty-tip'; +import Loading from '../../../components/loading'; +import Paginator from '../../../components/paginator'; +import MainPanelTopbar from '../main-panel-topbar'; +import UserLink from '../user-link'; +import LogsNav from './logs-nav'; class Content extends Component { @@ -44,9 +45,9 @@ class Content extends Component { <table className="table-hover"> <thead> <tr> - <th width="10%">{gettext('Share From')}</th> - <th width="10%">{gettext('Share To')}</th> - <th width="20%">{gettext('Actions')}</th> + <th width="15%">{gettext('Share From')}</th> + <th width="15%">{gettext('Share To')}</th> + <th width="10%">{gettext('Actions')}</th> <th width="13%">{gettext('Permission')}</th> <th width="20%">{gettext('Library')}</th> <th width="12%">{gettext('Folder')}</th> @@ -112,12 +113,27 @@ class Item extends Component { } } + getShareTo = (item) => { + switch(item.share_type) { + case 'user': + return <UserLink email={item.to_user_email} name={item.to_user_name} />; + case 'group': + return <Link to={`${siteRoot}sys/groups/${item.to_group_id}/libraries/`}>{item.to_group_name}</Link>; + case 'department': + return <Link to={`${siteRoot}sys/departments/${item.to_group_id}/`}>{item.to_group_name}</Link>; + case 'all': + return <Link to={`${siteRoot}org/`}>{gettext('All')}</Link>; + default: + return gettext('Deleted'); + } + } + render() { let { item } = this.props; return ( <tr onMouseOver={this.handleMouseOver} onMouseOut={this.handleMouseOut}> <td><UserLink email={item.from_user_email} name={item.from_user_name} /></td> - <td><UserLink email={item.to_user_email} name={item.to_user_name} /></td> + <td>{this.getShareTo(item)}</td> <td>{this.getActionTextByEType(item.etype)}</td> <td>{Utils.sharePerms(item.permission)}</td> <td>{item.repo_name ? item.repo_name : gettext('Deleted')}</td> diff --git a/frontend/src/pages/sys-admin/statistic/traffic-organizations-table.js b/frontend/src/pages/sys-admin/statistic/statistic-traffic-orgs.js similarity index 69% rename from frontend/src/pages/sys-admin/statistic/traffic-organizations-table.js rename to frontend/src/pages/sys-admin/statistic/statistic-traffic-orgs.js index 0050cb2714..64204c8dbf 100644 --- a/frontend/src/pages/sys-admin/statistic/traffic-organizations-table.js +++ b/frontend/src/pages/sys-admin/statistic/statistic-traffic-orgs.js @@ -10,19 +10,20 @@ import Loading from '../../../components/loading'; import { Utils } from '../../../utils/utils'; import toaster from '../../../components/toast'; -class TrafficOrganizationsTable extends React.Component { +class OrgsTraffic extends React.Component { constructor(props) { super(props); this.state = { - userTrafficList: [], + orgTrafficList: [], perPage: 25, currentPage: 1, hasNextPage: false, month: moment().format('YYYYMM'), isLoading: false, errorMessage: '', - sortOrder: 'asc' + sortBy: 'link_file_download', + sortOrder: 'desc' }; this.initPage = 1; this.initMonth = moment().format('YYYYMM'); @@ -35,16 +36,16 @@ class TrafficOrganizationsTable extends React.Component { perPage: parseInt(urlParams.get('per_page') || perPage), currentPage: parseInt(urlParams.get('page') || currentPage) }, () => { - this.onGenerateReports(this.initMonth, this.state.currentPage); + this.getTrafficList(this.initMonth, this.state.currentPage); }); } getPreviousPage = () => { - this.onGenerateReports(this.state.month, this.state.currentPage - 1); + this.getTrafficList(this.state.month, this.state.currentPage - 1); } getNextPage = () => { - this.onGenerateReports(this.state.month, this.state.currentPage + 1); + this.getTrafficList(this.state.month, this.state.currentPage + 1); } handleChange = (e) => { @@ -65,28 +66,22 @@ class TrafficOrganizationsTable extends React.Component { }); return; } - this.onGenerateReports(month, this.initPage); + this.getTrafficList(month, this.initPage); e.target.blur(); e.preventDefault(); } } - sortBySize = (sortByType, sortOrder) => { - let { userTrafficList } = this.state; - let newUserTrafficList = Utils.sortTraffic(userTrafficList, sortByType, sortOrder); - this.setState({ - userTrafficList: newUserTrafficList, - sortOrder: sortOrder - }); - } - - onGenerateReports = (month, page) => { - let { perPage } = this.state; + getTrafficList = (month, page) => { + const { perPage, sortBy, sortOrder } = this.state; + const orderBy = `${sortBy}_${sortOrder}`; this.setState({isLoading: true, errorMessage: ''}); - seafileAPI.sysAdminListOrgTraffic(month, page, perPage).then(res => { - let userTrafficList = res.data.org_monthly_traffic_list.slice(0); + seafileAPI.sysAdminListOrgTraffic(month, page, perPage, orderBy).then(res => { + let orgTrafficList = res.data.org_monthly_traffic_list.slice(0); this.setState({ - userTrafficList: userTrafficList, + month: month, + currentPage: page, + orgTrafficList: orgTrafficList, hasNextPage: res.data.has_next_page, isLoading: false }); @@ -96,14 +91,28 @@ class TrafficOrganizationsTable extends React.Component { }); } + sortItems = (sortBy) => { + this.setState({ + sortBy: sortBy, + sortOrder: this.state.sortOrder == 'asc' ? 'desc' : 'asc' + }, () => { + const { month, currentPage } = this.state; + this.getTrafficList(month, currentPage); + }); + } + resetPerPage = (newPerPage) => { this.setState({ perPage: newPerPage, - }, () => this.onGenerateReports(this.initPage, this.initMonth)); + }, () => this.getTrafficList(this.initPage, this.initMonth)); } render() { - let { userTrafficList, currentPage, hasNextPage, perPage, isLoading, errorMessage, sortOrder } = this.state; + const { + isLoading, errorMessage, orgTrafficList, + currentPage, hasNextPage, perPage, + sortBy, sortOrder + } = this.state; return ( <Fragment> <div className="d-flex align-items-center mt-4"> @@ -118,8 +127,8 @@ class TrafficOrganizationsTable extends React.Component { </div> {isLoading && <Loading />} {!isLoading && - <TrafficTable type={'org'} sortOrder={sortOrder} sortBySize={this.sortBySize} > - {userTrafficList.length > 0 && userTrafficList.map((item, index) => { + <TrafficTable type={'org'} sortItems={this.sortItems} sortBy={sortBy} sortOrder={sortOrder}> + {orgTrafficList.length > 0 && orgTrafficList.map((item, index) => { return( <TrafficTableBody key={index} @@ -143,4 +152,4 @@ class TrafficOrganizationsTable extends React.Component { } } -export default TrafficOrganizationsTable; +export default OrgsTraffic; diff --git a/frontend/src/pages/sys-admin/statistic/traffic-user-table.js b/frontend/src/pages/sys-admin/statistic/statistic-traffic-users.js similarity index 73% rename from frontend/src/pages/sys-admin/statistic/traffic-user-table.js rename to frontend/src/pages/sys-admin/statistic/statistic-traffic-users.js index 5456472e47..8988c64887 100644 --- a/frontend/src/pages/sys-admin/statistic/traffic-user-table.js +++ b/frontend/src/pages/sys-admin/statistic/statistic-traffic-users.js @@ -10,7 +10,7 @@ import { gettext } from '../../../utils/constants'; import { Utils } from '../../../utils/utils'; import toaster from '../../../components/toast'; -class TrafficOrganizationsTable extends React.Component { +class UsersTraffic extends React.Component { constructor(props) { super(props); @@ -22,7 +22,8 @@ class TrafficOrganizationsTable extends React.Component { month: moment().format('YYYYMM'), isLoading: false, errorMessage: '', - sortOrder: 'asc' + sortBy: 'link_file_download', + sortOrder: 'desc' }; this.initPage = 1; this.initMonth = moment().format('YYYYMM'); @@ -35,16 +36,16 @@ class TrafficOrganizationsTable extends React.Component { perPage: parseInt(urlParams.get('per_page') || perPage), currentPage: parseInt(urlParams.get('page') || currentPage) }, () => { - this.onGenerateReports(this.initMonth, this.state.currentPage); - }); + this.getTrafficList(this.initMonth, this.state.currentPage); + }); } getPreviousPage = () => { - this.onGenerateReports(this.state.month, this.state.currentPage - 1); + this.getTrafficList(this.state.month, this.state.currentPage - 1); } getNextPage = () => { - this.onGenerateReports(this.state.month, this.state.currentPage + 1); + this.getTrafficList(this.state.month, this.state.currentPage + 1); } handleChange = (e) => { @@ -54,15 +55,6 @@ class TrafficOrganizationsTable extends React.Component { }); } - sortBySize = (sortByType, sortOrder) => { - let { userTrafficList } = this.state; - let newUserTrafficList = Utils.sortTraffic(userTrafficList, sortByType, sortOrder); - this.setState({ - userTrafficList: newUserTrafficList, - sortOrder: sortOrder - }); - } - handleKeyPress = (e) => { let { month } = this.state; if (e.key === 'Enter') { @@ -74,21 +66,24 @@ class TrafficOrganizationsTable extends React.Component { }); return; } - this.onGenerateReports(month, this.initPage); + this.getTrafficList(month, this.initPage); e.target.blur(); e.preventDefault(); } } - onGenerateReports = (month, page) => { - let { perPage } = this.state; + getTrafficList = (month, page) => { + const { perPage, sortBy, sortOrder } = this.state; + const orderBy = `${sortBy}_${sortOrder}`; this.setState({ isLoading: true, errorMessage: '' }); - seafileAPI.sysAdminListUserTraffic(month, page, perPage).then(res => { + seafileAPI.sysAdminListUserTraffic(month, page, perPage, orderBy).then(res => { let userTrafficList = res.data.user_monthly_traffic_list.slice(0); this.setState({ + month: month, + currentPage: page, userTrafficList: userTrafficList, hasNextPage: res.data.has_next_page, isLoading: false @@ -99,14 +94,28 @@ class TrafficOrganizationsTable extends React.Component { }); } + sortItems = (sortBy) => { + this.setState({ + sortBy: sortBy, + sortOrder: this.state.sortOrder == 'asc' ? 'desc' : 'asc' + }, () => { + const { month, currentPage } = this.state; + this.getTrafficList(month, currentPage); + }); + } + resetPerPage = (newPerPage) => { this.setState({ perPage: newPerPage, - }, () => this.onGenerateReports(this.initMonth, this.initPage)); + }, () => this.getTrafficList(this.initMonth, this.initPage)); } render() { - let { userTrafficList, currentPage, hasNextPage, perPage, isLoading, errorMessage, sortOrder } = this.state; + const { + isLoading, errorMessage, userTrafficList, + currentPage, hasNextPage, perPage, + sortBy, sortOrder + } = this.state; return ( <Fragment> <div className="d-flex align-items-center mt-4"> @@ -121,7 +130,7 @@ class TrafficOrganizationsTable extends React.Component { </div> {isLoading && <Loading />} {!isLoading && - <TrafficTable type={'user'} sortBySize={this.sortBySize} sortOrder={sortOrder}> + <TrafficTable type={'user'} sortItems={this.sortItems} sortBy={sortBy} sortOrder={sortOrder}> {userTrafficList.length > 0 && userTrafficList.map((item, index) => { return( <TrafficTableBody @@ -146,4 +155,4 @@ class TrafficOrganizationsTable extends React.Component { } } -export default TrafficOrganizationsTable; +export default UsersTraffic; diff --git a/frontend/src/pages/sys-admin/statistic/statistic-traffic.js b/frontend/src/pages/sys-admin/statistic/statistic-traffic.js index d2137d65c6..873e98acf1 100644 --- a/frontend/src/pages/sys-admin/statistic/statistic-traffic.js +++ b/frontend/src/pages/sys-admin/statistic/statistic-traffic.js @@ -6,8 +6,8 @@ import MainPanelTopbar from '../main-panel-topbar'; import StatisticNav from './statistic-nav'; import StatisticCommonTool from './statistic-common-tool'; import Loading from '../../../components/loading'; -import TrafficOrganizationsTable from './traffic-organizations-table'; -import TrafficUserTable from './traffic-user-table'; +import OrgsTraffic from './statistic-traffic-orgs'; +import UsersTraffic from './statistic-traffic-users'; import StatisticChart from './statistic-chart'; import { Utils } from '../../../utils/utils'; import toaster from '../../../components/toast'; @@ -204,10 +204,10 @@ class StatisticTraffic extends React.Component { </div> } {!isLoading && tabActive === 'user' && - <TrafficUserTable /> + <UsersTraffic /> } {!isLoading && tabActive === 'organizations' && - <TrafficOrganizationsTable /> + <OrgsTraffic /> } </div> </div> diff --git a/frontend/src/pages/sys-admin/statistic/traffic-table.js b/frontend/src/pages/sys-admin/statistic/traffic-table.js index 762708fd4f..34bcb944a1 100644 --- a/frontend/src/pages/sys-admin/statistic/traffic-table.js +++ b/frontend/src/pages/sys-admin/statistic/traffic-table.js @@ -4,6 +4,9 @@ import { gettext } from '../../../utils/constants'; const propTypes = { type: PropTypes.string.isRequired, + sortBy: PropTypes.string.isRequired, + sortOrder: PropTypes.string.isRequired, + sortItems: PropTypes.func.isRequired, children: PropTypes.oneOfType([PropTypes.bool, PropTypes.array]), }; @@ -11,30 +14,10 @@ class TrafficTable extends React.Component { constructor(props) { super(props); - this.state = { - showIconName: 'link_file_download' - }; - } - - componentDidMount() { - let { showIconName } = this.state; - let { sortOrder } = this.props; - this.props.sortBySize(showIconName, sortOrder); - } - - sortBySize = (sortByType) => { - let { sortOrder } = this.props; - let newSortOrder = sortOrder === 'asc' ? 'desc' : 'asc'; - this.setState({ - showIconName: sortByType - }); - - this.props.sortBySize(sortByType, newSortOrder); } render() { - const { type, sortOrder } = this.props; - const { showIconName } = this.state; + const { type, sortBy, sortOrder } = this.props; const sortIcon = sortOrder == 'asc' ? <span className="fas fa-caret-up"></span> : <span className="fas fa-caret-down"></span>; return ( @@ -42,12 +25,12 @@ class TrafficTable extends React.Component { <thead> <tr> <th width="16%">{type == 'user' ? gettext('User') : gettext('Organization')}</th> - <th width="11%"><div className="d-block table-sort-op cursor-pointer" onClick={this.sortBySize.bind(this, 'sync_file_upload')}>{gettext('Sync Upload')} {showIconName === 'sync_file_upload' && sortIcon}</div></th> - <th width="14%"><div className="d-block table-sort-op cursor-pointer" onClick={this.sortBySize.bind(this, 'sync_file_donwload')}>{gettext('Sync Download')} {showIconName === 'sync_file_donwload' && sortIcon}</div></th> - <th width="11%"><div className="d-block table-sort-op cursor-pointer" onClick={this.sortBySize.bind(this, 'web_file_upload')}>{gettext('Web Upload')} {showIconName === 'web_file_upload' && sortIcon}</div></th> - <th width="14%"><div className="d-block table-sort-op cursor-pointer" onClick={this.sortBySize.bind(this, 'web_file_download')}>{gettext('Web Download')} {showIconName === 'web_file_download' && sortIcon}</div></th> - <th width="17%"><div className="d-block table-sort-op cursor-pointer" onClick={this.sortBySize.bind(this, 'link_file_upload')}>{gettext('Share link upload')} {showIconName === 'link_file_upload' && sortIcon}</div></th> - <th width="17%"><div className="d-block table-sort-op cursor-pointer" onClick={this.sortBySize.bind(this, 'link_file_download')}>{gettext('Share link download')} {showIconName === 'link_file_download' && sortIcon}</div></th> + <th width="11%"><div className="d-block table-sort-op cursor-pointer" onClick={this.props.sortItems.bind(this, 'sync_file_upload')}>{gettext('Sync Upload')} {sortBy === 'sync_file_upload' && sortIcon}</div></th> + <th width="14%"><div className="d-block table-sort-op cursor-pointer" onClick={this.props.sortItems.bind(this, 'sync_file_download')}>{gettext('Sync Download')} {sortBy === 'sync_file_download' && sortIcon}</div></th> + <th width="11%"><div className="d-block table-sort-op cursor-pointer" onClick={this.props.sortItems.bind(this, 'web_file_upload')}>{gettext('Web Upload')} {sortBy === 'web_file_upload' && sortIcon}</div></th> + <th width="14%"><div className="d-block table-sort-op cursor-pointer" onClick={this.props.sortItems.bind(this, 'web_file_download')}>{gettext('Web Download')} {sortBy === 'web_file_download' && sortIcon}</div></th> + <th width="17%"><div className="d-block table-sort-op cursor-pointer" onClick={this.props.sortItems.bind(this, 'link_file_upload')}>{gettext('Share link upload')} {sortBy === 'link_file_upload' && sortIcon}</div></th> + <th width="17%"><div className="d-block table-sort-op cursor-pointer" onClick={this.props.sortItems.bind(this, 'link_file_download')}>{gettext('Share link download')} {sortBy === 'link_file_download' && sortIcon}</div></th> </tr> </thead> <tbody> diff --git a/frontend/src/utils/utils.js b/frontend/src/utils/utils.js index 044ad1f3d6..0ee128ae9a 100644 --- a/frontend/src/utils/utils.js +++ b/frontend/src/utils/utils.js @@ -1042,24 +1042,6 @@ export const Utils = { return items; }, - sortTraffic(items, sortBy, sortOrder) { - let comparator; - switch(sortOrder) { - case 'asc': - comparator = function(a, b) { - return a[sortBy] < b[sortBy] ? -1 : 1; - }; - break; - case 'desc': - comparator = function(a, b) { - return a[sortBy] < b[sortBy] ? 1 : -1; - }; - break; - } - items.sort(comparator); - return items; - }, - /* * only used in the 'catch' part of a seafileAPI request */ diff --git a/requirements.txt b/requirements.txt index c58f27db9f..c3acf608ba 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,7 @@ openpyxl qrcode django-formtools django-simple-captcha -djangorestframework +djangorestframework==3.11.1 python-dateutil requests pillow diff --git a/seahub/api2/endpoints/admin/logs.py b/seahub/api2/endpoints/admin/logs.py index b39663f699..32ff95e2ad 100644 --- a/seahub/api2/endpoints/admin/logs.py +++ b/seahub/api2/endpoints/admin/logs.py @@ -310,40 +310,55 @@ class AdminLogsSharePermissionLogs(APIView): to_nickname_dict = {} to_contact_email_dict = {} repo_dict = {} + to_group_name_dict = {} + from_user_email_set = set() to_user_email_set = set() repo_id_set = set() + to_group_id_set = set() + department_set = set() for event in events: + from_user_email_set.add(event.from_user) - to_user_email_set.add(event.to) repo_id_set.add(event.repo_id) + if is_valid_email(event.to): + to_user_email_set.add(event.to) + + if event.to.isdigit(): + to_group_id_set.add(event.to) + for e in from_user_email_set: if e not in from_nickname_dict: from_nickname_dict[e] = email2nickname(e) if e not in from_contact_email_dict: from_contact_email_dict[e] = email2contact_email(e) + for e in to_user_email_set: if e not in to_nickname_dict: to_nickname_dict[e] = email2nickname(e) if e not in to_contact_email_dict: to_contact_email_dict[e] = email2contact_email(e) + for e in repo_id_set: if e not in repo_dict: repo_dict[e] = seafile_api.get_repo(e) + for group_id in to_group_id_set: + if group_id not in to_group_name_dict: + group = ccnet_api.get_group(int(group_id)) + to_group_name_dict[group_id] = group.group_name + if group.parent_group_id != 0: + department_set.add(group_id) + events_info = [] for ev in events: data = {} from_user_email = ev.from_user - to_user_email = ev.to data['from_user_email'] = from_user_email data['from_user_name'] = from_nickname_dict.get(from_user_email, '') data['from_user_contact_email'] = from_contact_email_dict.get(from_user_email, '') - data['to_user_email'] = to_user_email - data['to_user_name'] = to_nickname_dict.get(to_user_email, '') - data['to_user_contact_email'] = to_contact_email_dict.get(to_user_email, '') data['etype'] = ev.etype data['permission'] = ev.permission @@ -355,6 +370,33 @@ class AdminLogsSharePermissionLogs(APIView): data['folder'] = '/' if ev.file_path == '/' else os.path.basename(ev.file_path.rstrip('/')) data['date'] = utc_datetime_to_isoformat_timestr(ev.timestamp) + + data['share_type'] = 'all' + + data['to_user_email'] = '' + data['to_user_name'] = '' + data['to_user_contact_email'] = '' + data['to_group_id'] = '' + data['to_group_name'] = '' + + if is_valid_email(ev.to): + to_user_email = ev.to + data['to_user_email'] = to_user_email + data['to_user_name'] = to_nickname_dict.get(to_user_email, '') + data['to_user_contact_email'] = to_contact_email_dict.get(to_user_email, '') + data['share_type'] = 'user' + + if ev.to.isdigit(): + + to_group_id = ev.to + data['to_group_id'] = to_group_id + data['to_group_name'] = to_group_name_dict.get(to_group_id, '') + + if to_group_id in department_set: + data['share_type'] = 'department' + else: + data['share_type'] = 'group' + events_info.append(data) resp = { diff --git a/seahub/api2/endpoints/admin/statistics.py b/seahub/api2/endpoints/admin/statistics.py index 7ff5e0c55f..34f2030fbd 100644 --- a/seahub/api2/endpoints/admin/statistics.py +++ b/seahub/api2/endpoints/admin/statistics.py @@ -241,9 +241,22 @@ class SystemUserTrafficView(APIView): per_page = 25 start = (page - 1) * per_page + order_by = request.GET.get('order_by', '') + filters = [ + 'sync_file_upload', 'sync_file_download', + 'web_file_upload', 'web_file_download', + 'link_file_upload', 'link_file_download', + ] + if order_by not in filters and \ + order_by not in map(lambda x: x + '_desc', filters): + order_by = 'link_file_download_desc' + # get one more item than per_page, to judge has_next_page try: - traffics = seafevents_api.get_all_users_traffic_by_month(month_obj, start, start + per_page + 1) + traffics = seafevents_api.get_all_users_traffic_by_month(month_obj, + start, + start + per_page + 1, + order_by) except Exception as e: logger.error(e) error_msg = 'Internal Server Error' @@ -305,9 +318,22 @@ class SystemOrgTrafficView(APIView): per_page = 25 start = (page - 1) * per_page + order_by = request.GET.get('order_by', '') + filters = [ + 'sync_file_upload', 'sync_file_download', + 'web_file_upload', 'web_file_download', + 'link_file_upload', 'link_file_download', + ] + if order_by not in filters and \ + order_by not in map(lambda x: x + '_desc', filters): + order_by = 'link_file_download_desc' + # get one more item than per_page, to judge has_next_page try: - traffics = seafevents_api.get_all_orgs_traffic_by_month(month_obj, start, start + per_page + 1) + traffics = seafevents_api.get_all_orgs_traffic_by_month(month_obj, + start, + start + per_page + 1, + order_by) except Exception as e: logger.error(e) error_msg = 'Internal Server Error' diff --git a/seahub/api2/endpoints/admin/users.py b/seahub/api2/endpoints/admin/users.py index 78348ffbd4..ff51ae71e4 100644 --- a/seahub/api2/endpoints/admin/users.py +++ b/seahub/api2/endpoints/admin/users.py @@ -849,7 +849,7 @@ class AdminUser(APIView): login_id = login_id.strip() username_by_login_id = Profile.objects.get_username_by_login_id(login_id) if username_by_login_id is not None: - return api_error(status.HTTP_400_BAD_REQUEST, + return api_error(status.HTTP_400_BAD_REQUEST, _("Login id %s already exists." % login_id)) contact_email = request.data.get("contact_email", None) @@ -860,7 +860,7 @@ class AdminUser(APIView): password = request.data.get("password") - reference_id = request.data.get("reference_id", "") + reference_id = request.data.get("reference_id", None) if reference_id: if ' ' in reference_id: return api_error(status.HTTP_400_BAD_REQUEST, 'Reference ID can not contain spaces.') diff --git a/seahub/api2/endpoints/auth_token_by_session.py b/seahub/api2/endpoints/auth_token_by_session.py new file mode 100644 index 0000000000..bd58b32c57 --- /dev/null +++ b/seahub/api2/endpoints/auth_token_by_session.py @@ -0,0 +1,23 @@ +# Copyright (c) 2012-2016 Seafile Ltd. +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 seahub.api2.throttling import UserRateThrottle +from seahub.api2.utils import get_token_v1 + + +class AuthTokenBySession(APIView): + """ Get user's auth token. + """ + + authentication_classes = (SessionAuthentication,) + permission_classes = (IsAuthenticated,) + throttle_classes = (UserRateThrottle,) + + def get(self, request): + + token = get_token_v1(request.user.username) + + return Response({'token': token.key}) diff --git a/seahub/api2/endpoints/repo_send_new_password.py b/seahub/api2/endpoints/repo_send_new_password.py index af7f590c77..2388e70694 100644 --- a/seahub/api2/endpoints/repo_send_new_password.py +++ b/seahub/api2/endpoints/repo_send_new_password.py @@ -9,7 +9,7 @@ from rest_framework import status from django.utils.translation import ugettext as _ from django.utils.crypto import get_random_string -from seaserv import seafile_api +from seaserv import seafile_api, ccnet_api from seahub.api2.authentication import TokenAuthentication from seahub.api2.throttling import UserRateThrottle @@ -17,14 +17,15 @@ from seahub.api2.utils import api_error from seahub.api2.views import HTTP_520_OPERATION_FAILED from seahub.utils import IS_EMAIL_CONFIGURED, send_html_email -from seahub.utils.repo import is_repo_owner +from seahub.utils.repo import get_repo_owner from seahub.base.models import RepoSecretKey -from seahub.base.templatetags.seahub_tags import email2contact_email +from seahub.base.templatetags.seahub_tags import email2contact_email, email2nickname from seahub.settings import ENABLE_RESET_ENCRYPTED_REPO_PASSWORD logger = logging.getLogger(__name__) + class RepoSendNewPassword(APIView): authentication_classes = (TokenAuthentication, SessionAuthentication) @@ -55,12 +56,21 @@ class RepoSendNewPassword(APIView): return api_error(status.HTTP_400_BAD_REQUEST, error_msg) # permission check - username = request.user.username - if not is_repo_owner(request, repo_id, username): - error_msg = 'Permission denied.' - return api_error(status.HTTP_403_FORBIDDEN, error_msg) - secret_key = RepoSecretKey.objects.get_secret_key(repo_id) + username = request.user.username + repo_owner = get_repo_owner(request, repo_id) + + if '@seafile_group' in repo_owner: + group_id = email2nickname(repo_owner) + if not ccnet_api.check_group_staff(int(group_id), username): + error_msg = 'Permission denied.' + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + else: + if username != repo_owner: + error_msg = 'Permission denied.' + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + + secret_key = RepoSecretKey.objects.get_secret_key(repo_id) if not secret_key: error_msg = _("Can not reset this library's password.") return api_error(HTTP_520_OPERATION_FAILED, error_msg) @@ -68,10 +78,10 @@ class RepoSendNewPassword(APIView): new_password = get_random_string(10) try: seafile_api.reset_repo_passwd(repo_id, username, secret_key, new_password) - content = {'repo_name': repo.name, 'password': new_password,} + content = {'repo_name': repo.name, 'password': new_password} send_html_email(_('New password of library %s') % repo.name, - 'snippets/reset_repo_password.html', content, - None, [email2contact_email(username)]) + 'snippets/reset_repo_password.html', content, + None, [email2contact_email(username)]) except Exception as e: logger.error(e) error_msg = 'Internal Server Error' diff --git a/seahub/api2/endpoints/repo_set_password.py b/seahub/api2/endpoints/repo_set_password.py index 15c09bc149..9e23d6cf20 100644 --- a/seahub/api2/endpoints/repo_set_password.py +++ b/seahub/api2/endpoints/repo_set_password.py @@ -8,15 +8,17 @@ from rest_framework.views import APIView from rest_framework import status from django.utils.translation import ugettext as _ -from seaserv import seafile_api +from seaserv import seafile_api, ccnet_api from pysearpc import SearpcError 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_repo_owner, add_encrypted_repo_secret_key_to_database +from seahub.utils.repo import is_repo_owner, get_repo_owner, \ + add_encrypted_repo_secret_key_to_database from seahub.base.models import RepoSecretKey from seahub.views import check_folder_permission +from seahub.base.templatetags.seahub_tags import email2nickname from seahub.settings import ENABLE_RESET_ENCRYPTED_REPO_PASSWORD @@ -103,10 +105,19 @@ class RepoSetPassword(APIView): return api_error(status.HTTP_400_BAD_REQUEST, error_msg) # permission check + username = request.user.username - if not is_repo_owner(request, repo_id, username): - error_msg = 'Permission denied.' - return api_error(status.HTTP_403_FORBIDDEN, error_msg) + repo_owner = get_repo_owner(request, repo_id) + + if '@seafile_group' in repo_owner: + group_id = email2nickname(repo_owner) + if not ccnet_api.check_group_staff(int(group_id), username): + error_msg = 'Permission denied.' + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + else: + if username != repo_owner: + error_msg = 'Permission denied.' + return api_error(status.HTTP_403_FORBIDDEN, error_msg) if operation == 'change-password': diff --git a/seahub/onlyoffice/utils.py b/seahub/onlyoffice/utils.py index 52c49eed94..f8124e3a71 100644 --- a/seahub/onlyoffice/utils.py +++ b/seahub/onlyoffice/utils.py @@ -10,38 +10,45 @@ from django.utils.encoding import force_bytes from seaserv import seafile_api +from seahub.base.templatetags.seahub_tags import email2nickname from seahub.utils import get_file_type_and_ext, gen_file_get_url, \ get_site_scheme_and_netloc, normalize_cache_key from seahub.settings import ENABLE_WATERMARK from seahub.onlyoffice.settings import ONLYOFFICE_APIJS_URL, \ - ONLYOFFICE_FORCE_SAVE + ONLYOFFICE_FORCE_SAVE, ONLYOFFICE_JWT_SECRET + def generate_onlyoffice_cache_key(repo_id, file_path): prefix = "ONLYOFFICE_" value = "%s_%s" % (repo_id, file_path) return normalize_cache_key(value, prefix) -def get_onlyoffice_dict(username, repo_id, file_path, - file_id='', can_edit=False, can_download=True): + +def get_onlyoffice_dict(request, username, repo_id, file_path, file_id='', + can_edit=False, can_download=True): repo = seafile_api.get_repo(repo_id) if repo.is_virtual: origin_repo_id = repo.origin_repo_id - origin_file_path = posixpath.join(repo.origin_path, file_path.strip('/')) + origin_file_path = posixpath.join(repo.origin_path, + file_path.strip('/')) # for view history/trash/snapshot file if not file_id: file_id = seafile_api.get_file_id_by_path(origin_repo_id, - origin_file_path) + origin_file_path) else: origin_repo_id = repo_id origin_file_path = file_path if not file_id: file_id = seafile_api.get_file_id_by_path(repo_id, - file_path) + file_path) dl_token = seafile_api.get_fileserver_access_token(repo_id, - file_id, 'download', username, use_onetime=True) + file_id, + 'download', + username, + use_onetime=True) if not dl_token: return None @@ -62,9 +69,12 @@ def get_onlyoffice_dict(username, repo_id, file_path, doc_key = cache.get(cache_key) if not doc_key: - doc_key = hashlib.md5(force_bytes(origin_repo_id + origin_file_path + file_id)).hexdigest()[:20] + info_bytes = force_bytes(origin_repo_id + origin_file_path + file_id) + doc_key = hashlib.md5(info_bytes).hexdigest()[:20] - doc_info = json.dumps({'repo_id': repo_id, 'file_path': file_path, 'username': username}) + doc_info = json.dumps({'repo_id': repo_id, + 'file_path': file_path, + 'username': username}) cache.set("ONLYOFFICE_%s" % doc_key, doc_info, None) file_name = os.path.basename(file_path.rstrip('/')) @@ -72,7 +82,8 @@ def get_onlyoffice_dict(username, repo_id, file_path, base_url = get_site_scheme_and_netloc() onlyoffice_editor_callback_url = reverse('onlyoffice_editor_callback') - calllback_url = urllib.parse.urljoin(base_url, onlyoffice_editor_callback_url) + callback_url = urllib.parse.urljoin(base_url, + onlyoffice_editor_callback_url) return_dict = { 'repo_id': repo_id, @@ -83,7 +94,7 @@ def get_onlyoffice_dict(username, repo_id, file_path, 'doc_title': file_name, 'doc_url': doc_url, 'document_type': document_type, - 'callback_url': calllback_url, + 'callback_url': callback_url, 'can_edit': can_edit, 'can_download': can_download, 'username': username, @@ -91,4 +102,36 @@ def get_onlyoffice_dict(username, repo_id, file_path, 'enable_watermark': ENABLE_WATERMARK and not can_edit, } + if ONLYOFFICE_JWT_SECRET: + import jwt + config = { + "document": { + "fileType": fileext, + "key": doc_key, + "title": file_name, + "url": doc_url, + "permissions": { + "download": can_download, + "edit": can_edit, + "print": can_download, + "review": True + } + }, + "documentType": document_type, + "editorConfig": { + "callbackUrl": callback_url, + "lang": request.LANGUAGE_CODE, + "mode": can_edit, + "customization": { + "forcesave": ONLYOFFICE_FORCE_SAVE, + }, + "user": { + "name": email2nickname(username) + } + } + } + + return_dict['onlyoffice_jwt_token'] = jwt.encode(config, + ONLYOFFICE_JWT_SECRET) + return return_dict diff --git a/seahub/urls.py b/seahub/urls.py index 06e9994622..9c69298ac4 100644 --- a/seahub/urls.py +++ b/seahub/urls.py @@ -84,6 +84,7 @@ from seahub.api2.endpoints.activities import ActivitiesView from seahub.api2.endpoints.wiki_pages import WikiPagesDirView, WikiPageContentView from seahub.api2.endpoints.revision_tag import TaggedItemsView, TagNamesView from seahub.api2.endpoints.user import User +from seahub.api2.endpoints.auth_token_by_session import AuthTokenBySession from seahub.api2.endpoints.repo_tags import RepoTagsView, RepoTagView from seahub.api2.endpoints.file_tag import RepoFileTagsView, RepoFileTagView from seahub.api2.endpoints.tag_filter_file import TaggedFilesView @@ -278,6 +279,9 @@ urlpatterns = [ ## user url(r'^api/v2.1/user/$', User.as_view(), name="api-v2.1-user"), + ## obtain auth token by login session + url(r'^api/v2.1/auth-token-by-session/$', AuthTokenBySession.as_view(), name="api-v2.1-auth-token-by-session"), + ## user::smart-link url(r'^api/v2.1/smart-link/$', SmartLink.as_view(), name="api-v2.1-smart-link"), url(r'^api/v2.1/smart-links/(?P<token>[-0-9a-f]{36})/$', SmartLinkToken.as_view(), name="api-v2.1-smart-links-token"), diff --git a/seahub/views/__init__.py b/seahub/views/__init__.py index 24b88b77a6..6c87dfbfa4 100644 --- a/seahub/views/__init__.py +++ b/seahub/views/__init__.py @@ -9,7 +9,7 @@ import logging import posixpath from django.core.cache import cache -from django.urls import reverse +from django.urls import reverse, resolve from django.contrib import messages from django.http import HttpResponse, Http404, \ HttpResponseRedirect @@ -1120,10 +1120,47 @@ def choose_register(request): 'login_bg_image_path': login_bg_image_path }) + @login_required def react_fake_view(request, **kwargs): - + username = request.user.username + + if resolve(request.path).url_name == 'lib_view': + + repo_id = kwargs.get('repo_id', '') + path = kwargs.get('path', '') + + if repo_id and path and \ + not check_folder_permission(request, repo_id, path): + + converted_repo_path = seafile_api.convert_repo_path(repo_id, path, username) + if not converted_repo_path: + error_msg = 'Permission denied.' + return render_error(request, error_msg) + + repo_path_dict = json.loads(converted_repo_path) + + converted_repo_id = repo_path_dict['repo_id'] + converted_repo = seafile_api.get_repo(converted_repo_id) + if not converted_repo: + error_msg = 'Library %s not found.' % converted_repo_id + return render_error(request, error_msg) + + converted_path = repo_path_dict['path'] + if not seafile_api.get_dirent_by_path(converted_repo_id, converted_path): + error_msg = 'Dirent %s not found.' % converted_path + return render_error(request, error_msg) + + if not check_folder_permission(request, converted_repo_id, converted_path): + error_msg = 'Permission denied.' + return render_error(request, error_msg) + + next_url = reverse('lib_view', args=[converted_repo_id, + converted_repo.repo_name, + converted_path.strip('/')]) + return HttpResponseRedirect(next_url) + guide_enabled = UserOptions.objects.is_user_guide_enabled(username) if guide_enabled: create_default_library(request) @@ -1165,9 +1202,9 @@ def react_fake_view(request, **kwargs): 'is_email_configured': IS_EMAIL_CONFIGURED, 'can_add_public_repo': request.user.permissions.can_add_public_repo(), 'folder_perm_enabled': folder_perm_enabled, - 'file_audit_enabled' : FILE_AUDIT_ENABLED, - 'custom_nav_items' : json.dumps(CUSTOM_NAV_ITEMS), - 'enable_show_contact_email_when_search_user' : settings.ENABLE_SHOW_CONTACT_EMAIL_WHEN_SEARCH_USER, + 'file_audit_enabled': FILE_AUDIT_ENABLED, + 'custom_nav_items': json.dumps(CUSTOM_NAV_ITEMS), + 'enable_show_contact_email_when_search_user': settings.ENABLE_SHOW_CONTACT_EMAIL_WHEN_SEARCH_USER, 'additional_share_dialog_note': ADDITIONAL_SHARE_DIALOG_NOTE, 'additional_app_bottom_links': ADDITIONAL_APP_BOTTOM_LINKS, 'additional_about_dialog_links': ADDITIONAL_ABOUT_DIALOG_LINKS, diff --git a/seahub/views/file.py b/seahub/views/file.py index 10477240fa..075f78a9d5 100644 --- a/seahub/views/file.py +++ b/seahub/views/file.py @@ -125,11 +125,6 @@ try: except ImportError: ONLYOFFICE_EDIT_FILE_EXTENSION = () -try: - from seahub.onlyoffice.settings import ONLYOFFICE_JWT_SECRET -except ImportError: - ONLYOFFICE_JWT_SECRET = '' - # bisheng office from seahub.bisheng_office.utils import get_bisheng_dict, \ get_bisheng_editor_url, get_bisheng_preivew_url @@ -790,9 +785,8 @@ def view_lib_file(request, repo_id, path): (is_locked and locked_by_online_office)): can_edit = True - onlyoffice_dict = get_onlyoffice_dict(username, repo_id, path, - can_edit=can_edit, - can_download=parse_repo_perm(permission).can_download) + onlyoffice_dict = get_onlyoffice_dict(request, username, repo_id, path, + can_edit=can_edit, can_download=parse_repo_perm(permission).can_download) if onlyoffice_dict: if is_pro_version() and can_edit: @@ -806,36 +800,6 @@ def view_lib_file(request, repo_id, path): send_file_access_msg(request, repo, path, 'web') - if ONLYOFFICE_JWT_SECRET: - import jwt - config = { - "document": { - "fileType": onlyoffice_dict['file_type'], - "key": onlyoffice_dict['doc_key'], - "title": onlyoffice_dict['doc_title'], - "url": onlyoffice_dict['doc_url'], - "permissions": { - "download": onlyoffice_dict['can_download'], - "edit": onlyoffice_dict['can_edit'], - "print": onlyoffice_dict['can_download'], - "review": True - } - }, - "documentType": onlyoffice_dict['document_type'], - "editorConfig": { - "callbackUrl": onlyoffice_dict['callback_url'], - "lang": request.LANGUAGE_CODE, - "mode": onlyoffice_dict['can_edit'], - "customization": { - "forcesave": onlyoffice_dict['onlyoffice_force_save'], - }, - "user": { - "name": email2nickname(username) - } - } - }; - onlyoffice_dict['onlyoffice_jwt_token'] = jwt.encode(config, ONLYOFFICE_JWT_SECRET) - return render(request, 'view_file_onlyoffice.html', onlyoffice_dict) else: return_dict['err'] = _('Error when prepare OnlyOffice file preview page.') @@ -944,7 +908,7 @@ def view_history_file_common(request, repo_id, ret_dict): if ENABLE_ONLYOFFICE and fileext in ONLYOFFICE_FILE_EXTENSION: - onlyoffice_dict = get_onlyoffice_dict(username, repo_id, path, + onlyoffice_dict = get_onlyoffice_dict(request, username, repo_id, path, file_id=obj_id, can_download=parse_repo_perm(user_perm).can_download) if onlyoffice_dict: @@ -1230,7 +1194,7 @@ def view_shared_file(request, fileshare): if ENABLE_ONLYOFFICE and fileext in ONLYOFFICE_FILE_EXTENSION: - onlyoffice_dict = get_onlyoffice_dict(username, repo_id, path, + onlyoffice_dict = get_onlyoffice_dict(request, username, repo_id, path, can_edit=can_edit, can_download=can_download) if onlyoffice_dict: @@ -1416,7 +1380,7 @@ def view_file_via_shared_dir(request, fileshare): if ENABLE_ONLYOFFICE and fileext in ONLYOFFICE_FILE_EXTENSION: - onlyoffice_dict = get_onlyoffice_dict(username, + onlyoffice_dict = get_onlyoffice_dict(request, username, repo_id, real_path) if onlyoffice_dict: