1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-14 22:33:17 +00:00

Virus scan frontend (#4574)

* ignore virus scan record

* get virus record by has_handle

* add has_next_page

* change has_handle to has_deleted

* change url and view name

* delete or ignore virus files in batch

* rebase master

* add cancel-ignore

* optimize code

* add translate

* [virus scan] added 'All' and 'Unhandled' tabs

* rewrote 'all' page (previous 'virus-scan-records')
* added 'unhandled' page

* [virus scan] 'unhandled' tab: added 'delete & ignore selected items'

* [virus scan] 'unhandled' tab: added 'handle selected items'

Co-authored-by: 王健辉 <40563566+jianhw@users.noreply.github.com>
This commit is contained in:
llj
2020-07-09 14:15:17 +08:00
committed by GitHub
parent 72b1edd832
commit fa3964332f
10 changed files with 994 additions and 265 deletions

View File

@@ -7,6 +7,12 @@ import MainPanel from './main-panel';
import Info from './info'; import Info from './info';
import StatisticFile from './statistic/statistic-file';
import StatisticStorage from './statistic/statistic-storage';
import StatisticTraffic from './statistic/statistic-traffic';
import StatisticUsers from './statistic/statistic-users';
import StatisticReport from './statistic/statistic-reports';
import DesktopDevices from './devices/desktop-devices'; import DesktopDevices from './devices/desktop-devices';
import MobileDevices from './devices/mobile-devices'; import MobileDevices from './devices/mobile-devices';
import DeviceErrors from './devices/devices-errors'; import DeviceErrors from './devices/devices-errors';
@@ -58,24 +64,19 @@ import FileAccessLogs from './logs-page/file-access-logs';
import FileUpdateLogs from './logs-page/file-update-logs'; import FileUpdateLogs from './logs-page/file-update-logs';
import SharePermissionLogs from './logs-page/share-permission-logs'; import SharePermissionLogs from './logs-page/share-permission-logs';
import AdminOperationLogs from './admin-logs/operation-logs';
import AdminLoginLogs from './admin-logs/login-logs';
import TermsAndConditions from './terms-and-conditions/terms-and-conditions';
import WebSettings from './web-settings/web-settings'; import WebSettings from './web-settings/web-settings';
import Notifications from './notifications/notifications'; import Notifications from './notifications/notifications';
import FileScanRecords from './file-scan-records'; import FileScanRecords from './file-scan-records';
import VirusScanRecords from './virus-scan-records';
import WorkWeixinDepartments from './work-weixin-departments'; import WorkWeixinDepartments from './work-weixin-departments';
import DingtalkDepartments from './dingtalk-departments'; import DingtalkDepartments from './dingtalk-departments';
import Invitations from './invitations/invitations'; import Invitations from './invitations/invitations';
import TermsAndConditions from './terms-and-conditions/terms-and-conditions';
import StatisticFile from './statistic/statistic-file'; import AllVirusFiles from './virus-scan/all-virus-files';
import StatisticStorage from './statistic/statistic-storage'; import UnhandledVirusFiles from './virus-scan/unhandled-virus-files';
import StatisticTraffic from './statistic/statistic-traffic';
import StatisticUsers from './statistic/statistic-users'; import AdminOperationLogs from './admin-logs/operation-logs';
import StatisticReport from './statistic/statistic-reports'; import AdminLoginLogs from './admin-logs/login-logs';
import '../../assets/css/fa-solid.css'; import '../../assets/css/fa-solid.css';
import '../../assets/css/fa-regular.css'; import '../../assets/css/fa-regular.css';
@@ -141,6 +142,10 @@ class SysAdmin extends React.Component {
tab: 'logs', tab: 'logs',
urlPartList: ['logs/'] urlPartList: ['logs/']
}, },
{
tab: 'virus-files',
urlPartList: ['virus-files/']
},
{ {
tab: 'adminLogs', tab: 'adminLogs',
urlPartList: ['admin-logs/'] urlPartList: ['admin-logs/']
@@ -244,16 +249,15 @@ class SysAdmin extends React.Component {
<Invitations path={siteRoot + 'sys/invitations'} /> <Invitations path={siteRoot + 'sys/invitations'} />
<TermsAndConditions path={siteRoot + 'sys/terms-and-conditions/'} /> <TermsAndConditions path={siteRoot + 'sys/terms-and-conditions/'} />
<AllVirusFiles path={siteRoot + 'sys/virus-files/all'} />
<UnhandledVirusFiles path={siteRoot + 'sys/virus-files/unhandled'} />
<FileScanRecords <FileScanRecords
path={siteRoot + 'sys/file-scan-records'} path={siteRoot + 'sys/file-scan-records'}
currentTab={currentTab} currentTab={currentTab}
tabItemClick={this.tabItemClick} tabItemClick={this.tabItemClick}
/> />
<VirusScanRecords
path={siteRoot + 'sys/virus-scan-records'}
currentTab={currentTab}
tabItemClick={this.tabItemClick}
/>
<WorkWeixinDepartments <WorkWeixinDepartments
path={siteRoot + 'sys/work-weixin'} path={siteRoot + 'sys/work-weixin'}
currentTab={currentTab} currentTab={currentTab}

View File

@@ -202,9 +202,9 @@ class SidePanel extends React.Component {
{isPro && otherPermission && {isPro && otherPermission &&
<li className="nav-item"> <li className="nav-item">
<Link <Link
className={`nav-link ellipsis ${this.getActiveClass('virus-scan-records')}`} className={`nav-link ellipsis ${this.getActiveClass('virus-files')}`}
to={siteRoot + 'sys/virus-scan-records/'} to={siteRoot + 'sys/virus-files/all/'}
onClick={() => this.props.tabItemClick('virus-scan-records')} onClick={() => this.props.tabItemClick('virus-files')}
> >
<span className="sf2-icon-security" aria-hidden="true"></span> <span className="sf2-icon-security" aria-hidden="true"></span>
<span className="nav-text">{gettext('Virus Scan')}</span> <span className="nav-text">{gettext('Virus Scan')}</span>

View File

@@ -1,206 +0,0 @@
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import { seafileAPI } from '../../utils/seafile-api';
import { gettext } from '../../utils/constants';
import toaster from '../../components/toast';
import Account from '../../components/common/account';
const recordItemPropTypes = {
record: PropTypes.object.isRequired,
};
class RecordItem extends Component {
constructor(props) {
super(props);
this.state = {
handleStatus: this.props.record.has_handle,
errorMsg: '',
};
}
deleteVirusScanRecord = () => {
seafileAPI.deleteVirusScanRecord(this.props.record.virus_id).then(() => {
this.setState({
handleStatus: !this.state.handleStatus,
});
}).catch((error) => {
if (error.response) {
this.setState({
errorMsg: error.response.data.error_msg,
});
toaster.danger(this.state.errorMsg);
}
});
}
render() {
let record = this.props.record;
return (
<tr>
<td>{record.repo_name}</td>
<td>{record.repo_owner}</td>
<td>{record.file_path}</td>
<td>
{
this.state.handleStatus ?
<span style={{color: 'green'}}>{gettext('Handled')}</span> :
<a style={{color: 'red', cursor: 'pointer'}} onClick={this.deleteVirusScanRecord}>{gettext('Delete')}</a>
}
</td>
</tr>
);
}
}
RecordItem.propTypes = recordItemPropTypes;
const recordListPropTypes = {
loading: PropTypes.bool.isRequired,
isLoadingMore: PropTypes.bool.isRequired,
errorMsg: PropTypes.string.isRequired,
records: PropTypes.array.isRequired,
};
class RecordList extends Component {
render() {
let { loading, isLoadingMore, errorMsg, records } = this.props;
if (loading) {
return <span className="loading-icon loading-tip"></span>;
} else if (errorMsg) {
return <p className="error text-center">{errorMsg}</p>;
} else {
return (
<Fragment>
<table width="100%" className="table table-hover table-vcenter">
<thead>
<tr>
<th width="28%">{gettext('Library')}</th>
<th width="28%">{gettext('Owner')}</th>
<th width="29%">{gettext('Virus File')}</th>
<th width="15%">{gettext('Operations')}</th>
</tr>
</thead>
<tbody>
{records.map((record, index) => {
return (
<RecordItem key={index} record={record} />
);
})}
</tbody>
</table>
{isLoadingMore ? <span className="loading-icon loading-tip"></span> : ''}
</Fragment>
);
}
}
}
RecordList.propTypes = recordListPropTypes;
class VirusScanRecords extends Component {
constructor(props) {
super(props);
this.state = {
loading: true,
isLoadingMore: false,
currentPage: 1,
hasMore: true,
errorMsg: '',
records: [],
};
}
getMore() {
let currentPage = this.state.currentPage;
seafileAPI.listVirusScanRecords(currentPage).then((res) => {
this.setState({
isLoadingMore: false,
records: [...this.state.records, ...res.data.record_list],
currentPage: currentPage + 1,
hasMore: res.data.record_list.length === 0 ? false : true,
});
}).catch((error) => {
if (error.response) {
this.setState({
isLoadingMore: false,
errorMsg: error.response.data.error_msg,
});
toaster.danger(this.state.errorMsg);
}
});
}
handleScroll = (event) => {
if (!this.state.isLoadingMore && this.state.hasMore) {
const clientHeight = event.target.clientHeight;
const scrollHeight = event.target.scrollHeight;
const scrollTop = event.target.scrollTop;
const isBottom = (clientHeight + scrollTop + 1 >= scrollHeight);
if (isBottom) {
this.setState({isLoadingMore: true}, () => {
this.getMore();
});
}
}
}
componentDidMount() {
let currentPage = this.state.currentPage;
seafileAPI.listVirusScanRecords(currentPage).then((res) => {
this.setState({
loading: false,
records: res.data.record_list,
currentPage: currentPage + 1,
hasMore: true,
});
}).catch((error) => {
if (error.response) {
this.setState({
loading: false,
errorMsg: error.response.data.error_msg,
});
toaster.danger(this.state.errorMsg);
}
});
}
render() {
return (
<Fragment>
<div className="main-panel-north border-left-show">
<div className="cur-view-toolbar">
<span className="sf2-icon-menu side-nav-toggle hidden-md-up d-md-none" title="Side Nav Menu"></span>
</div>
<div className="common-toolbar">
<Account isAdminPanel={true} />
</div>
</div>
<div className="main-panel-center">
<div className="cur-view-container" id="content-scan-records">
<div className="cur-view-path">
<h3 className="sf-heading">{gettext('Virus Scan Records')}</h3>
</div>
<div className="cur-view-content" onScroll={this.handleScroll}>
<RecordList
loading={this.state.loading}
isLoadingMore={this.state.isLoadingMore}
errorMsg={this.state.errorMsg}
records={this.state.records}
/>
</div>
</div>
</div>
</Fragment>
);
}
}
export default VirusScanRecords;

View File

@@ -0,0 +1,313 @@
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import { seafileAPI } from '../../../utils/seafile-api';
import { gettext } from '../../../utils/constants';
import { Utils } from '../../../utils/utils';
import toaster from '../../../components/toast';
import OpMenu from '../../../components/dialog/op-menu';
import Loading from '../../../components/loading';
import Paginator from '../../../components/paginator';
import MainPanelTopbar from '../main-panel-topbar';
import Nav from './nav';
const virusFileItemPropTypes = {
virusFile: PropTypes.object.isRequired,
isItemFreezed: PropTypes.bool.isRequired,
onFreezedItem: PropTypes.func.isRequired,
onUnfreezedItem: PropTypes.func.isRequired
};
class VirusFileItem extends Component {
constructor(props) {
super(props);
this.state = {
highlight: false,
isOpIconShown: false
};
}
handleMouseEnter = () => {
if (!this.props.isItemFreezed) {
this.setState({
isOpIconShown: true,
highlight: true
});
}
}
handleMouseLeave = () => {
if (!this.props.isItemFreezed) {
this.setState({
isOpIconShown: false,
highlight: false
});
}
}
onUnfreezedItem = () => {
this.setState({
highlight: false,
isOpIconShow: false
});
this.props.onUnfreezedItem();
}
onMenuItemClick = (operation) => {
this.props.handleFile(this.props.virusFile.virus_id, operation);
}
translateOperations = (item) => {
let translateResult = '';
switch(item) {
case 'delete':
translateResult = gettext('Delete');
break;
case 'ignore':
translateResult = gettext('Ignore');
break;
case 'do-not-ignore':
translateResult = gettext('Don\'t ignore');
break;
}
return translateResult;
}
render() {
const virusFile = this.props.virusFile;
let fileStatus = '',
fileOpList = [];
if (virusFile.has_deleted) {
fileStatus = <span className="text-green">{gettext('Deleted')}</span>;
} else if (virusFile.has_ignored) {
fileStatus = <span className="text-orange">{gettext('Ignored')}</span>;
fileOpList = ['do-not-ignore'];
} else {
fileStatus = <span className="text-red">{gettext('Unhandled')}</span>;
fileOpList = ['delete', 'ignore'];
}
return (
<tr className={this.state.highlight ? 'tr-highlight' : ''} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
<td>{virusFile.repo_name}</td>
<td>{virusFile.repo_owner}</td>
<td>{virusFile.file_path}</td>
<td>{fileStatus}</td>
<td>
{fileOpList.length > 0 && this.state.isOpIconShown &&
<OpMenu
operations={fileOpList}
translateOperations={this.translateOperations}
onMenuItemClick={this.onMenuItemClick}
onFreezedItem={this.props.onFreezedItem}
onUnfreezedItem={this.onUnfreezedItem}
/>
}
</td>
</tr>
);
}
}
VirusFileItem.propTypes = virusFileItemPropTypes;
const virusFileListPropTypes = {
loading: PropTypes.bool.isRequired,
errorMsg: PropTypes.string.isRequired,
virusFiles: PropTypes.array.isRequired
};
class Content extends Component {
constructor(props) {
super(props);
this.state = {
isItemFreezed: false
};
}
onFreezedItem = () => {
this.setState({isItemFreezed: true});
}
onUnfreezedItem = () => {
this.setState({isItemFreezed: false});
}
getPreviousPage = () => {
this.props.getListByPage(this.props.currentPage - 1);
}
getNextPage = () => {
this.props.getListByPage(this.props.currentPage + 1);
}
render() {
const {
loading, errorMsg, virusFiles,
curPerPage, hasNextPage, currentPage
} = this.props;
if (loading) {
return <Loading />;
} else if (errorMsg) {
return <p className="error text-center mt-4">{errorMsg}</p>;
} else {
return (
<Fragment>
<table>
<thead>
<tr>
<th width="27%">{gettext('Library')}</th>
<th width="25%">{gettext('Owner')}</th>
<th width="28%">{gettext('Virus File')}</th>
<th width="15%">{gettext('Status')}</th>
<th width="5%">{/* Operations */}</th>
</tr>
</thead>
<tbody>
{virusFiles.map((virusFile, index) => {
return (
<VirusFileItem
key={index}
virusFile={virusFile}
isItemFreezed={this.state.isItemFreezed}
onFreezedItem={this.onFreezedItem}
onUnfreezedItem={this.onUnfreezedItem}
handleFile={this.props.handleFile}
/>
);
})}
</tbody>
</table>
{virusFiles.length > 0 &&
<Paginator
gotoPreviousPage={this.getPreviousPage}
gotoNextPage={this.getNextPage}
currentPage={currentPage}
hasNextPage={hasNextPage}
curPerPage={curPerPage}
resetPerPage={this.props.resetPerPage}
/>
}
</Fragment>
);
}
}
}
Content.propTypes = virusFileListPropTypes;
class AllVirusFiles extends Component {
constructor(props) {
super(props);
this.state = {
loading: true,
errorMsg: '',
virusFiles: [],
currentPage: 1,
perPage: 25,
hasNextPage: false
};
}
componentDidMount() {
let urlParams = (new URL(window.location)).searchParams;
const {
currentPage, perPage
} = this.state;
this.setState({
perPage: parseInt(urlParams.get('per_page') || perPage),
currentPage: parseInt(urlParams.get('page') || currentPage)
}, () => {
this.getListByPage(this.state.currentPage);
});
}
getListByPage = (page) => {
const { perPage } = this.state;
seafileAPI.listVirusFiles(page, perPage).then((res) => {
const data = res.data;
this.setState({
loading: false,
virusFiles: data.virus_file_list,
hasNextPage: data.has_next_page
});
}).catch((error) => {
this.setState({
loading: false,
errorMsg: Utils.getErrorMsg(error, true) // true: show login tip if 403
});
});
}
resetPerPage = (perPage) => {
this.setState({
perPage: perPage
}, () => {
this.getListByPage(1);
});
}
handleFile = (virusID, op) => {
let request;
switch(op) {
case 'delete':
request = seafileAPI.deleteVirusFile(virusID);
break;
case 'ignore':
request = seafileAPI.toggleIgnoreVirusFile(virusID, true);
break;
case 'do-not-ignore':
request = seafileAPI.toggleIgnoreVirusFile(virusID, false);
break;
}
request.then((res) => {
this.setState({
virusFiles: this.state.virusFiles.map((item) => {
if (item.virus_id == virusID) {
if (op == 'delete') {
item.has_deleted = true;
} else {
item = res.data.virus_file;
}
}
return item;
})
});
}).catch((error) => {
toaster.danger(Utils.getErrorMsg(error));
});
}
render() {
return (
<Fragment>
<MainPanelTopbar />
<div className="main-panel-center">
<div className="cur-view-container">
<Nav currentItem="all" />
<div className="cur-view-content">
<Content
loading={this.state.loading}
errorMsg={this.state.errorMsg}
virusFiles={this.state.virusFiles}
currentPage={this.state.currentPage}
hasNextPage={this.state.hasNextPage}
curPerPage={this.state.perPage}
resetPerPage={this.resetPerPage}
getListByPage={this.getListByPage}
handleFile={this.handleFile}
/>
</div>
</div>
</div>
</Fragment>
);
}
}
export default AllVirusFiles;

View File

@@ -0,0 +1,40 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Link } from '@reach/router';
import { siteRoot, gettext } from '../../../utils/constants';
const propTypes = {
currentItem: PropTypes.string.isRequired
};
class Nav extends React.Component {
constructor(props) {
super(props);
this.navItems = [
{name: 'all', urlPart: 'all', text: gettext('All')},
{name: 'unhandled', urlPart: 'unhandled', text: gettext('Unhandled')}
];
}
render() {
const { currentItem } = this.props;
return (
<div className="cur-view-path tab-nav-container">
<ul className="nav">
{this.navItems.map((item, index) => {
return (
<li className="nav-item" key={index}>
<Link to={`${siteRoot}sys/virus-files/${item.urlPart}/`} className={`nav-link${currentItem == item.name ? ' active' : ''}`}>{item.text}</Link>
</li>
);
})}
</ul>
</div>
);
}
}
Nav.propTypes = propTypes;
export default Nav;

View File

@@ -0,0 +1,421 @@
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import { Button } from 'reactstrap';
import { seafileAPI } from '../../../utils/seafile-api';
import { gettext } from '../../../utils/constants';
import { Utils } from '../../../utils/utils';
import toaster from '../../../components/toast';
import OpMenu from '../../../components/dialog/op-menu';
import Loading from '../../../components/loading';
import Paginator from '../../../components/paginator';
import MainPanelTopbar from '../main-panel-topbar';
import Nav from './nav';
const virusFileItemPropTypes = {
virusFile: PropTypes.object.isRequired,
isItemFreezed: PropTypes.bool.isRequired,
onFreezedItem: PropTypes.func.isRequired,
onUnfreezedItem: PropTypes.func.isRequired
};
class VirusFileItem extends Component {
constructor(props) {
super(props);
this.state = {
highlight: false,
isOpIconShown: false
};
}
handleMouseEnter = () => {
if (!this.props.isItemFreezed) {
this.setState({
isOpIconShown: true,
highlight: true
});
}
}
handleMouseLeave = () => {
if (!this.props.isItemFreezed) {
this.setState({
isOpIconShown: false,
highlight: false
});
}
}
onUnfreezedItem = () => {
this.setState({
highlight: false,
isOpIconShow: false
});
this.props.onUnfreezedItem();
}
onMenuItemClick = (operation) => {
this.props.handleFile(this.props.virusFile.virus_id, operation);
}
translateOperations = (item) => {
let translateResult = '';
switch(item) {
case 'delete':
translateResult = gettext('Delete');
break;
case 'ignore':
translateResult = gettext('Ignore');
break;
case 'do-not-ignore':
translateResult = gettext('Don\'t ignore');
break;
}
return translateResult;
}
toggleItemSelected = (e) => {
this.props.toggleItemSelected(this.props.virusFile, e.target.checked);
}
render() {
const virusFile = this.props.virusFile;
let fileStatus = '',
fileOpList = [];
if (virusFile.has_deleted) {
fileStatus = <span className="text-green">{gettext('Deleted')}</span>;
} else if (virusFile.has_ignored) {
fileStatus = <span className="text-orange">{gettext('Ignored')}</span>;
fileOpList = ['do-not-ignore'];
} else {
fileStatus = <span className="text-red">{gettext('Unhandled')}</span>;
fileOpList = ['delete', 'ignore'];
}
return (
<tr className={this.state.highlight ? 'tr-highlight' : ''} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
<td className="text-center">
<input type="checkbox" checked={virusFile.isSelected} onChange={this.toggleItemSelected} />
</td>
<td>{virusFile.repo_name}</td>
<td>{virusFile.repo_owner}</td>
<td>{virusFile.file_path}</td>
<td>{fileStatus}</td>
<td>
{fileOpList.length > 0 && this.state.isOpIconShown &&
<OpMenu
operations={fileOpList}
translateOperations={this.translateOperations}
onMenuItemClick={this.onMenuItemClick}
onFreezedItem={this.props.onFreezedItem}
onUnfreezedItem={this.onUnfreezedItem}
/>
}
</td>
</tr>
);
}
}
VirusFileItem.propTypes = virusFileItemPropTypes;
const virusFileListPropTypes = {
loading: PropTypes.bool.isRequired,
errorMsg: PropTypes.string.isRequired,
virusFiles: PropTypes.array.isRequired
};
class Content extends Component {
constructor(props) {
super(props);
this.state = {
isItemFreezed: false
};
}
onFreezedItem = () => {
this.setState({isItemFreezed: true});
}
onUnfreezedItem = () => {
this.setState({isItemFreezed: false});
}
getPreviousPage = () => {
this.props.getListByPage(this.props.currentPage - 1);
}
getNextPage = () => {
this.props.getListByPage(this.props.currentPage + 1);
}
render() {
const {
loading, errorMsg, virusFiles,
curPerPage, hasNextPage, currentPage,
isAllItemsSelected
} = this.props;
if (loading) {
return <Loading />;
} else if (errorMsg) {
return <p className="error text-center mt-4">{errorMsg}</p>;
} else {
return (
<Fragment>
<table>
<thead>
<tr>
<th width="3%" className="text-center">
<input type="checkbox" checked={isAllItemsSelected} onChange={this.props.toggleAllSelected} />
</th>
<th width="24%">{gettext('Library')}</th>
<th width="25%">{gettext('Owner')}</th>
<th width="28%">{gettext('Virus File')}</th>
<th width="15%">{gettext('Status')}</th>
<th width="5%">{/* Operations */}</th>
</tr>
</thead>
<tbody>
{virusFiles.map((virusFile, index) => {
return (
<VirusFileItem
key={index}
virusFile={virusFile}
isItemFreezed={this.state.isItemFreezed}
onFreezedItem={this.onFreezedItem}
onUnfreezedItem={this.onUnfreezedItem}
handleFile={this.props.handleFile}
toggleItemSelected={this.props.toggleItemSelected}
/>
);
})}
</tbody>
</table>
{virusFiles.length > 0 &&
<Paginator
gotoPreviousPage={this.getPreviousPage}
gotoNextPage={this.getNextPage}
currentPage={currentPage}
hasNextPage={hasNextPage}
curPerPage={curPerPage}
resetPerPage={this.props.resetPerPage}
/>
}
</Fragment>
);
}
}
}
Content.propTypes = virusFileListPropTypes;
class UnhandledVirusFiles extends Component {
constructor(props) {
super(props);
this.state = {
loading: true,
errorMsg: '',
virusFiles: [],
isAllItemsSelected: false,
selectedItems: [],
currentPage: 1,
perPage: 25,
hasNextPage: false
};
}
componentDidMount() {
let urlParams = (new URL(window.location)).searchParams;
const {
currentPage, perPage
} = this.state;
this.setState({
perPage: parseInt(urlParams.get('per_page') || perPage),
currentPage: parseInt(urlParams.get('page') || currentPage)
}, () => {
this.getListByPage(this.state.currentPage);
});
}
getListByPage = (page) => {
const { perPage } = this.state;
const hasHandled = false;
seafileAPI.listVirusFiles(page, perPage, hasHandled).then((res) => {
const data = res.data;
const items = data.virus_file_list.map(item => {
item.isSelected = false;
return item;
});
this.setState({
loading: false,
virusFiles: items,
hasNextPage: data.has_next_page
});
}).catch((error) => {
this.setState({
loading: false,
errorMsg: Utils.getErrorMsg(error, true) // true: show login tip if 403
});
});
}
resetPerPage = (perPage) => {
this.setState({
perPage: perPage
}, () => {
this.getListByPage(1);
});
}
handleFile = (virusID, op) => {
let request;
switch(op) {
case 'delete':
request = seafileAPI.deleteVirusFile(virusID);
break;
case 'ignore':
request = seafileAPI.toggleIgnoreVirusFile(virusID, true);
break;
case 'do-not-ignore':
request = seafileAPI.toggleIgnoreVirusFile(virusID, false);
break;
}
request.then((res) => {
this.setState({
virusFiles: this.state.virusFiles.map((item) => {
if (item.virus_id == virusID) {
if (op == 'delete') {
item.has_deleted = true;
} else {
item = res.data.virus_file;
}
}
return item;
})
});
}).catch((error) => {
toaster.danger(Utils.getErrorMsg(error));
});
}
toggleAllSelected = () => {
this.setState((prevState) => ({
isAllItemsSelected: !prevState.isAllItemsSelected,
virusFiles: this.state.virusFiles.map((item) => {
item.isSelected = !prevState.isAllItemsSelected;
return item;
})
}));
}
toggleItemSelected = (targetItem, isSelected) => {
this.setState({
virusFiles: this.state.virusFiles.map((item) => {
if (item === targetItem) {
item.isSelected = isSelected;
}
return item;
})
}, () => {
this.setState({
isAllItemsSelected: !this.state.virusFiles.some(item => !item.isSelected)
});
});
}
handleSelectedItems = (op) => {
// op: 'delete-virus', 'ignore-virus'
const virusIDs = this.state.virusFiles
.filter(item => {
if (op == 'delete-virus') {
return item.isSelected && !item.has_deleted;
} else {
return item.isSelected && !item.has_ignored;
}
})
.map(item => item.virus_id);
seafileAPI.batchProcessVirusFiles(virusIDs, op).then((res) => {
let fileList = this.state.virusFiles;
res.data.success.forEach(item => {
let file = fileList.find(file => file.virus_id == item.virus_id);
if (op == 'delete-virus') {
file.has_deleted = true;
} else {
file.has_ignored = true;
}
});
this.setState({
virusFiles: fileList
});
res.data.failed.forEach(item => {
const file = fileList.find(file => file.virus_id == item.virus_id);
let errMsg = op == 'delete-virus' ?
gettext('Failed to delete %(virus_file) from library %(library): %(error_msg)') :
gettext('Failed to ignore %(virus_file) from library %(library): %(error_msg)');
errMsg = errMsg.replace('%(virus_file)', file.file_path)
.replace('%(library)', file.repo_name)
.replace('%(error_msg)', item.error_msg);
toaster.danger(errMsg);
});
}).catch((error) => {
toaster.danger(Utils.getErrorMsg(error));
});
}
deleteSelectedItems = () => {
const op = 'delete-virus';
this.handleSelectedItems(op);
}
ignoreSelectedItems = () => {
const op = 'ignore-virus';
this.handleSelectedItems(op);
}
render() {
return (
<Fragment>
{this.state.virusFiles.some(item => item.isSelected) ? (
<MainPanelTopbar>
<Fragment>
<Button onClick={this.deleteSelectedItems} className="operation-item">{gettext('Delete')}</Button>
<Button onClick={this.ignoreSelectedItems} className="operation-item">{gettext('Ignore')}</Button>
</Fragment>
</MainPanelTopbar>
) : <MainPanelTopbar />
}
<div className="main-panel-center">
<div className="cur-view-container">
<Nav currentItem="unhandled" />
<div className="cur-view-content">
<Content
loading={this.state.loading}
errorMsg={this.state.errorMsg}
virusFiles={this.state.virusFiles}
currentPage={this.state.currentPage}
hasNextPage={this.state.hasNextPage}
curPerPage={this.state.perPage}
resetPerPage={this.resetPerPage}
getListByPage={this.getListByPage}
handleFile={this.handleFile}
isAllItemsSelected={this.state.isAllItemsSelected}
toggleAllSelected={this.toggleAllSelected}
toggleItemSelected={this.toggleItemSelected}
/>
</div>
</div>
</div>
</Fragment>
);
}
}
export default UnhandledVirusFiles;

View File

@@ -7,26 +7,27 @@ from rest_framework.permissions import IsAdminUser
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.views import APIView from rest_framework.views import APIView
from rest_framework import status from rest_framework import status
from django.utils.translation import ugettext as _
from seaserv import seafile_api from seaserv import seafile_api
from seahub.api2.authentication import TokenAuthentication from seahub.api2.authentication import TokenAuthentication
from seahub.api2.throttling import UserRateThrottle from seahub.api2.throttling import UserRateThrottle
from seahub.api2.permissions import IsProVersion from seahub.api2.permissions import IsProVersion
from seahub.api2.utils import api_error from seahub.api2.utils import api_error, to_python_boolean
from seahub.utils import get_virus_record, get_virus_record_by_id, handle_virus_record from seahub.utils import get_virus_files, get_virus_file_by_vid, delete_virus_file, operate_virus_file
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class AdminVirusScanRecords(APIView): class AdminVirusFilesView(APIView):
authentication_classes = (TokenAuthentication, SessionAuthentication) authentication_classes = (TokenAuthentication, SessionAuthentication)
permission_classes = (IsAdminUser, IsProVersion) permission_classes = (IsAdminUser, IsProVersion)
throttle_classes = (UserRateThrottle,) throttle_classes = (UserRateThrottle,)
def get(self, request): def get(self, request):
"""get virus scan records """get virus files
""" """
if not request.user.admin_permissions.other_permission(): if not request.user.admin_permissions.other_permission():
@@ -42,21 +43,32 @@ class AdminVirusScanRecords(APIView):
except ValueError: except ValueError:
per_page = 25 per_page = 25
try:
has_handled = to_python_boolean(request.GET.get('has_handled', ''))
except ValueError:
has_handled = None
start = (page - 1) * per_page start = (page - 1) * per_page
count = per_page count = per_page + 1
try: try:
virus_records = get_virus_record(start=start, limit=count) virus_files = get_virus_files(has_handled=has_handled, start=start, limit=count)
except Exception as e: except Exception as e:
logger.error(e) logger.error(e)
error_msg = 'Internal Server Error' error_msg = 'Internal Server Error'
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
record_list = list() if len(virus_files) > per_page:
for virus_record in virus_records: virus_files = virus_files[:per_page]
has_next_page = True
else:
has_next_page = False
virus_file_list = list()
for virus_file in virus_files:
try: try:
repo = seafile_api.get_repo(virus_record.repo_id) repo = seafile_api.get_repo(virus_file.repo_id)
repo_owner = seafile_api.get_repo_owner(virus_record.repo_id) repo_owner = seafile_api.get_repo_owner(virus_file.repo_id)
except Exception as e: except Exception as e:
logger.error(e) logger.error(e)
continue continue
@@ -67,40 +79,176 @@ class AdminVirusScanRecords(APIView):
record = dict() record = dict()
record["repo_name"] = repo.name record["repo_name"] = repo.name
record["repo_owner"] = repo_owner record["repo_owner"] = repo_owner
record["file_path"] = virus_record.file_path record["file_path"] = virus_file.file_path
record["has_handle"] = virus_record.has_handle record["has_deleted"] = virus_file.has_deleted
record["virus_id"] = virus_record.vid record["has_ignored"] = virus_file.has_ignored
record_list.append(record) record["virus_id"] = virus_file.vid
virus_file_list.append(record)
return Response({"record_list": record_list}, status=status.HTTP_200_OK) return Response({"virus_file_list": virus_file_list, "has_next_page": has_next_page}, status=status.HTTP_200_OK)
class AdminVirusScanRecord(APIView): class AdminVirusFileView(APIView):
authentication_classes = (TokenAuthentication, SessionAuthentication) authentication_classes = (TokenAuthentication, SessionAuthentication)
permission_classes = (IsAdminUser, IsProVersion) permission_classes = (IsAdminUser, IsProVersion)
throttle_classes = (UserRateThrottle,) throttle_classes = (UserRateThrottle,)
def delete(self, request, virus_id): def delete(self, request, virus_id):
"""delete virus file
"""
if not request.user.admin_permissions.other_permission(): if not request.user.admin_permissions.other_permission():
return api_error(status.HTTP_403_FORBIDDEN, 'Permission denied.') return api_error(status.HTTP_403_FORBIDDEN, 'Permission denied.')
virus_record = get_virus_record_by_id(virus_id) virus_file = get_virus_file_by_vid(virus_id)
if not virus_record: if not virus_file:
error_msg = 'Virus record %d not found.' % virus_id error_msg = 'Virus file %s not found.' % virus_id
return api_error(status.HTTP_404_NOT_FOUND, error_msg) return api_error(status.HTTP_404_NOT_FOUND, error_msg)
parent_dir = os.path.dirname(virus_record.file_path) parent_dir = os.path.dirname(virus_file.file_path)
filename = os.path.basename(virus_record.file_path) filename = os.path.basename(virus_file.file_path)
try: try:
seafile_api.del_file( seafile_api.del_file(
virus_record.repo_id, parent_dir, filename, request.user.username virus_file.repo_id, parent_dir, filename, request.user.username
) )
handle_virus_record(virus_id) delete_virus_file(virus_id)
except Exception as e: except Exception as e:
logger.error(e) logger.error(e)
error_msg = 'Failed to delete, please try again later.' error_msg = 'Internal Server Error'
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
return Response({"success": True}, status=status.HTTP_200_OK) return Response({"success": True}, status=status.HTTP_200_OK)
def put(self, request, virus_id):
"""ignore or un-ignore virus file
"""
if not request.user.admin_permissions.other_permission():
return api_error(status.HTTP_403_FORBIDDEN, 'Permission denied.')
ignore = request.data.get('ignore')
if ignore not in ('true', 'false'):
error_msg = 'ignore invalid.'
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
ignore = to_python_boolean(ignore)
virus_file = get_virus_file_by_vid(virus_id)
if not virus_file:
error_msg = 'Virus file %s not found.' % virus_id
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
try:
operate_virus_file(virus_id, ignore)
except Exception as e:
logger.error(e)
error_msg = 'Internal Server Error'
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
virus_file = get_virus_file_by_vid(virus_id)
repo = seafile_api.get_repo(virus_file.repo_id)
repo_owner = seafile_api.get_repo_owner(virus_file.repo_id)
res = dict(repo_name=repo.name)
res["repo_owner"] = repo_owner
res["file_path"] = virus_file.file_path
res["has_deleted"] = virus_file.has_deleted
res["has_ignored"] = virus_file.has_ignored
res["virus_id"] = virus_file.vid
return Response({"virus_file": res}, status=status.HTTP_200_OK)
class AdminVirusFilesBatchView(APIView):
authentication_classes = (TokenAuthentication, SessionAuthentication)
permission_classes = (IsAdminUser, IsProVersion)
throttle_classes = (UserRateThrottle,)
def post(self, request):
""" Delete virus files, ignore or cancel ignore virus files, in batch.
Permission checking:
1. admin user.
"""
if not request.user.admin_permissions.other_permission():
return api_error(status.HTTP_403_FORBIDDEN, 'Permission denied.')
# argument check
virus_ids = request.POST.getlist('virus_ids', None)
if not virus_ids:
error_msg = 'virus_ids invalid.'
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
operation = request.POST.get('operation', None)
if operation not in ('delete-virus', 'ignore-virus', 'cancel-ignore-virus'):
error_msg = "operation can only be 'delete-virus', 'ignore-virus' or 'cancel-ignore-virus'."
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
result = dict(failed=[])
result['success'] = []
virus_files = []
for virus_id in virus_ids:
virus_file = get_virus_file_by_vid(int(virus_id))
if virus_file:
virus_files.append(virus_file)
else:
result['failed'].append({
'virus_id': virus_id,
'error_msg': _('Virus file not found.')
})
continue
if operation == 'delete-virus':
for virus_file in virus_files:
parent_dir = os.path.dirname(virus_file.file_path)
filename = os.path.basename(virus_file.file_path)
virus_id = int(virus_file.vid)
try:
seafile_api.del_file(
virus_file.repo_id, parent_dir, filename, request.user.username
)
delete_virus_file(virus_id)
except Exception as e:
logger.error(e)
result['failed'].append({
'virus_id': virus_id,
'error_msg': _('Internal Server Error')
})
continue
result['success'].append({'virus_id': virus_id})
if operation == 'ignore-virus':
for virus_file in virus_files:
virus_id = int(virus_file.vid)
try:
operate_virus_file(virus_id, True)
except Exception as e:
logger.error(e)
result['failed'].append({
'virus_id': virus_id,
'error_msg': _('Internal Server Error')
})
continue
result['success'].append({'virus_id': virus_id})
if operation == 'cancel-ignore-virus':
for virus_file in virus_files:
virus_id = int(virus_file.vid)
try:
operate_virus_file(virus_id, False)
except Exception as e:
logger.error(e)
result['failed'].append({
'virus_id': virus_id,
'error_msg': _('Internal Server Error')
})
continue
result['success'].append({'virus_id': virus_id})
return Response(result)

View File

@@ -171,7 +171,8 @@ from seahub.api2.endpoints.admin.work_weixin import AdminWorkWeixinDepartments,
from seahub.api2.endpoints.admin.dingtalk import AdminDingtalkDepartments, \ from seahub.api2.endpoints.admin.dingtalk import AdminDingtalkDepartments, \
AdminDingtalkDepartmentMembers, AdminDingtalkUsersBatch, \ AdminDingtalkDepartmentMembers, AdminDingtalkUsersBatch, \
AdminDingtalkDepartmentsImport AdminDingtalkDepartmentsImport
from seahub.api2.endpoints.admin.virus_scan_records import AdminVirusScanRecords, AdminVirusScanRecord from seahub.api2.endpoints.admin.virus_scan_records import AdminVirusFilesView, AdminVirusFileView, \
AdminVirusFilesBatchView
from seahub.api2.endpoints.file_participants import FileParticipantsView, FileParticipantView from seahub.api2.endpoints.file_participants import FileParticipantsView, FileParticipantView
from seahub.api2.endpoints.repo_related_users import RepoRelatedUsersView from seahub.api2.endpoints.repo_related_users import RepoRelatedUsersView
@@ -614,9 +615,10 @@ urlpatterns = [
## admin::file-scan-records ## admin::file-scan-records
url(r'^api/v2.1/admin/file-scan-records/$', AdminFileScanRecords.as_view(), name='api-v2.1-admin-file-scan-records'), url(r'^api/v2.1/admin/file-scan-records/$', AdminFileScanRecords.as_view(), name='api-v2.1-admin-file-scan-records'),
# admin::virus-scan-records # admin::virus-files
url(r'^api/v2.1/admin/virus-scan-records/$', AdminVirusScanRecords.as_view(), name='api-v2.1-admin-virus-scan-records'), url(r'^api/v2.1/admin/virus-files/$', AdminVirusFilesView.as_view(), name='api-v2.1-admin-virus-files'),
url(r'^api/v2.1/admin/virus-scan-records/(?P<virus_id>\d+)/$', AdminVirusScanRecord.as_view(), name='api-v2.1-admin-virus-scan-record'), url(r'^api/v2.1/admin/virus-files/(?P<virus_id>\d+)/$', AdminVirusFileView.as_view(), name='api-v2.1-admin-virus-file'),
url(r'^api/v2.1/admin/virus-files/batch/$', AdminVirusFilesBatchView.as_view(), name='api-v2.1-admin-virus-files-batch'),
## admin::notifications ## admin::notifications
url(r'^api/v2.1/admin/notifications/$', AdminNotificationsView.as_view(), name='api-2.1-admin-notifications'), url(r'^api/v2.1/admin/notifications/$', AdminNotificationsView.as_view(), name='api-2.1-admin-notifications'),
@@ -731,7 +733,8 @@ if ENABLE_FILE_SCAN:
from seahub.utils import EVENTS_ENABLED from seahub.utils import EVENTS_ENABLED
if EVENTS_ENABLED: if EVENTS_ENABLED:
urlpatterns += [ urlpatterns += [
url(r'^sys/virus-scan-records/$', sysadmin_react_fake_view, name='sys_virus_scan_records'), url(r'^sys/virus-files/all/$', sysadmin_react_fake_view, name='sys_virus_scan_records'),
url(r'^sys/virus-files/unhandled/$', sysadmin_react_fake_view, name='sys_virus_scan_records'),
] ]
if settings.SERVE_STATIC: if settings.SERVE_STATIC:

View File

@@ -823,18 +823,22 @@ if EVENTS_CONFIG_FILE:
return events if events else None return events if events else None
def get_virus_record(repo_id=None, start=-1, limit=-1): def get_virus_files(repo_id=None, has_handled=None, start=-1, limit=-1):
with _get_seafevents_session() as session: with _get_seafevents_session() as session:
r = seafevents.get_virus_record(session, repo_id, start, limit) r = seafevents.get_virus_files(session, repo_id, has_handled, start, limit)
return r if r else [] return r if r else []
def handle_virus_record(vid): def delete_virus_file(vid):
with _get_seafevents_session() as session: with _get_seafevents_session() as session:
return True if seafevents.handle_virus_record(session, vid) == 0 else False return True if seafevents.delete_virus_file(session, vid) == 0 else False
def get_virus_record_by_id(vid): def operate_virus_file(vid, ignore):
with _get_seafevents_session() as session: with _get_seafevents_session() as session:
return seafevents.get_virus_record_by_id(session, vid) return True if seafevents.operate_virus_file(session, vid, ignore) == 0 else False
def get_virus_file_by_vid(vid):
with _get_seafevents_session() as session:
return seafevents.get_virus_file_by_vid(session, vid)
def get_file_scan_record(start=-1, limit=-1): def get_file_scan_record(start=-1, limit=-1):
records = seafevents_api.get_content_scan_results(start, limit) records = seafevents_api.get_content_scan_results(start, limit)
@@ -876,11 +880,13 @@ else:
pass pass
def get_perm_audit_events(): def get_perm_audit_events():
pass pass
def get_virus_record(): def get_virus_files():
pass pass
def handle_virus_record(): def delete_virus_file():
pass pass
def get_virus_record_by_id(vid): def operate_virus_file():
pass
def get_virus_file_by_vid(vid):
pass pass
def get_file_scan_record(): def get_file_scan_record():
pass pass

View File

@@ -48,8 +48,8 @@ from seahub.role_permissions.models import AdminRole
from seahub.two_factor.models import default_device from seahub.two_factor.models import default_device
from seahub.utils import IS_EMAIL_CONFIGURED, string2list, is_valid_username, \ from seahub.utils import IS_EMAIL_CONFIGURED, string2list, is_valid_username, \
is_pro_version, send_html_email, \ is_pro_version, send_html_email, \
get_server_id, handle_virus_record, get_virus_record_by_id, \ get_server_id, delete_virus_file, get_virus_file_by_vid, \
get_virus_record, FILE_AUDIT_ENABLED, get_max_upload_file_size, \ get_virus_files, FILE_AUDIT_ENABLED, get_max_upload_file_size, \
get_site_name, seafevents_api get_site_name, seafevents_api
from seahub.utils.ip import get_remote_ip from seahub.utils.ip import get_remote_ip
from seahub.utils.file_size import get_file_size_unit from seahub.utils.file_size import get_file_size_unit