diff --git a/frontend/config/webpack.config.dev.js b/frontend/config/webpack.config.dev.js
index 9f110abce8..30cadb6e63 100644
--- a/frontend/config/webpack.config.dev.js
+++ b/frontend/config/webpack.config.dev.js
@@ -143,7 +143,12 @@ module.exports = {
require.resolve('./polyfills'),
require.resolve('react-dev-utils/webpackHotDevClient'),
paths.appSrc + "/view-file-umind.js",
- ]
+ ],
+ sysAdmin: [
+ require.resolve('./polyfills'),
+ require.resolve('react-dev-utils/webpackHotDevClient'),
+ paths.appSrc + "/pages/sys-admin",
+ ],
},
output: {
diff --git a/frontend/config/webpack.config.prod.js b/frontend/config/webpack.config.prod.js
index 20348e98c7..ddbb9d506f 100644
--- a/frontend/config/webpack.config.prod.js
+++ b/frontend/config/webpack.config.prod.js
@@ -77,6 +77,7 @@ module.exports = {
viewFilePDF: [require.resolve('./polyfills'), paths.appSrc + "/view-file-pdf.js"],
orgAdmin: [require.resolve('./polyfills'), paths.appSrc + "/pages/org-admin"],
viewFileUMind: [require.resolve('./polyfills'), paths.appSrc + "/view-file-umind.js"],
+ sysAdmin: [require.resolve('./polyfills'), paths.appSrc + "/pages/sys-admin"],
},
output: {
diff --git a/frontend/src/components/common/account.js b/frontend/src/components/common/account.js
index e05c5ee7e2..3ec9f1d667 100644
--- a/frontend/src/components/common/account.js
+++ b/frontend/src/components/common/account.js
@@ -1,9 +1,14 @@
import React, { Component } from 'react';
+import PropTypes from 'prop-types';
import ReactDOM from 'react-dom';
import { Utils } from '../../utils/utils';
import editorUtilities from '../../utils/editor-utilties';
import { siteRoot, gettext } from '../../utils/constants';
+const propTypes = {
+ isAdminPanel: PropTypes.bool,
+};
+
class Account extends Component {
constructor(props) {
super(props);
@@ -87,11 +92,16 @@ class Account extends Component {
}
renderMenu = () => {
- if(this.state.isStaff){
+ if (this.state.isStaff && !this.props.isAdminPanel) {
return (
{gettext('System Admin')}
);
}
+ if (this.props.isAdminPanel) {
+ return (
+ {gettext('Exit Admin Panel')}
+ );
+ }
if (this.state.isOrgStaff) {
return (
{gettext('Organization Admin')}
@@ -143,4 +153,6 @@ class Account extends Component {
}
}
+Account.propTypes = propTypes;
+
export default Account;
diff --git a/frontend/src/pages/sys-admin/file-scan-records.js b/frontend/src/pages/sys-admin/file-scan-records.js
new file mode 100644
index 0000000000..b113ec0dad
--- /dev/null
+++ b/frontend/src/pages/sys-admin/file-scan-records.js
@@ -0,0 +1,137 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import { seafileAPI } from '../../utils/seafile-api';
+import { gettext, loginUrl } from '../../utils/constants';
+
+
+const tablePropTypes = {
+ loading: PropTypes.bool.isRequired,
+ errorMsg: PropTypes.string.isRequired,
+ records: PropTypes.array.isRequired,
+};
+
+class Table extends Component {
+
+ render() {
+ let { loading, errorMsg, records } = this.props;
+
+ if (loading) {
+ return ;
+ } else if (errorMsg) {
+ return
{errorMsg}
;
+ } else {
+ return (
+
+
+
+ {gettext('Library')} |
+ {gettext('ID')} |
+ {gettext('Path')} |
+ {gettext('Label')} |
+ {gettext('Suggestion')} |
+
+
+
+ {records.map((record, index) => {
+ return (
+
+ );
+ })}
+
+
+ );
+ }
+ }
+}
+
+Table.propTypes = tablePropTypes;
+
+
+const itemPropTypes = {
+ record: PropTypes.object.isRequired,
+};
+
+class Item extends Component {
+ constructor(props) {
+ super(props);
+ this.state = {};
+ }
+
+ render() {
+ let record = this.props.record;
+
+ return (
+
+ {record.repo_name} |
+ {record.repo_id} |
+ {record.path} |
+ {record.detail.label} |
+ {record.detail.suggestion} |
+
+ );
+ }
+}
+
+Item.propTypes = itemPropTypes;
+
+
+class FileScanRecords extends Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ loading: true,
+ errorMsg: '',
+ records: [],
+ };
+ }
+
+ componentDidMount() {
+ seafileAPI.listFileScanRecords().then((res) => {
+ this.setState({
+ loading: false,
+ records: res.data.record_list,
+ });
+ }).catch((error) => {
+ if (error.response) {
+ if (error.response.status == 403) {
+ this.setState({
+ loading: false,
+ errorMsg: gettext('Permission denied')
+ });
+ location.href = `${loginUrl}?next=${encodeURIComponent(location.href)}`;
+ } else {
+ this.setState({
+ loading: false,
+ errorMsg: gettext('Error')
+ });
+ }
+ } else {
+ this.setState({
+ loading: false,
+ errorMsg: gettext('Please check the network.')
+ });
+ }
+ });
+ }
+
+ render() {
+ return (
+
+
+
+
{gettext('Content Scan Records')}
+
+
+
+
+ );
+ }
+}
+
+export default FileScanRecords;
diff --git a/frontend/src/pages/sys-admin/index.js b/frontend/src/pages/sys-admin/index.js
new file mode 100644
index 0000000000..7207322eb8
--- /dev/null
+++ b/frontend/src/pages/sys-admin/index.js
@@ -0,0 +1,60 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import { Router } from '@reach/router';
+import { siteRoot, gettext } from '../../utils/constants';
+import SidePanel from './side-panel';
+import MainPanel from './main-panel';
+import FileScanRecords from './file-scan-records';
+
+import '../../assets/css/fa-solid.css';
+import '../../assets/css/fa-regular.css';
+import '../../assets/css/fontawesome.css';
+import '../../css/layout.css';
+import '../../css/toolbar.css';
+
+class SysAdmin extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ isSidePanelClosed: false,
+ currentTab: 'file-scan',
+ };
+ }
+
+ componentDidMount() {
+ let href = window.location.href.split('/');
+ this.setState({currentTab: href[href.length - 2]});
+ }
+
+ onCloseSidePanel = () => {
+ this.setState({isSidePanelClosed: !this.state.isSidePanelClosed});
+ }
+
+ tabItemClick = (param) => {
+ this.setState({currentTab: param});
+ }
+
+ render() {
+ let { currentTab, isSidePanelClosed, } = this.state;
+
+ return (
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+ReactDOM.render(
+ ,
+ document.getElementById('wrapper')
+);
diff --git a/frontend/src/pages/sys-admin/main-panel.js b/frontend/src/pages/sys-admin/main-panel.js
new file mode 100644
index 0000000000..ebe86a42ed
--- /dev/null
+++ b/frontend/src/pages/sys-admin/main-panel.js
@@ -0,0 +1,31 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import Account from '../../components/common/account';
+
+
+const propTypes = {
+ children: PropTypes.object.isRequired,
+};
+
+class MainPanel extends Component {
+
+ render() {
+ return (
+
+
+ {this.props.children}
+
+ );
+ }
+}
+
+MainPanel.propTypes = propTypes;
+
+export default MainPanel;
diff --git a/frontend/src/pages/sys-admin/side-panel.js b/frontend/src/pages/sys-admin/side-panel.js
new file mode 100644
index 0000000000..2beec7e00c
--- /dev/null
+++ b/frontend/src/pages/sys-admin/side-panel.js
@@ -0,0 +1,183 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Link } from '@reach/router';
+import Logo from '../../components/logo';
+import { gettext, siteRoot, isPro, isDefaultAdmin, canViewSystemInfo, canViewStatistic,
+ canConfigSystem, canManageLibrary, canManageUser, canManageGroup, canViewUserLog,
+ canViewAdminLog, constanceEnabled, multiTenancy, multiInstitution, sysadminExtraEnabled,
+ enableGuestInvitation, enableTermsAndConditions, enableFileScan } from '../../utils/constants';
+
+const propTypes = {
+ isSidePanelClosed: PropTypes.bool.isRequired,
+ onCloseSidePanel: PropTypes.func.isRequired,
+};
+
+class SidePanel extends React.Component {
+
+ render() {
+ return (
+
+
+
+
+
+
+
+
{gettext('System Admin')}
+
+
+
+
+
+ );
+ }
+}
+
+SidePanel.propTypes = propTypes;
+
+export default SidePanel;
diff --git a/frontend/src/utils/constants.js b/frontend/src/utils/constants.js
index c799c70327..f1704451fe 100644
--- a/frontend/src/utils/constants.js
+++ b/frontend/src/utils/constants.js
@@ -73,3 +73,21 @@ export const originFileExists = window.draft ? window.draft.config.originFileExi
// org admin
export const orgID = window.org ? window.org.pageOptions.orgID : '';
export const invitationLink = window.org ? window.org.pageOptions.invitationLink : '';
+
+// sys admin
+export const constanceEnabled = window.sysadmin ? window.sysadmin.pageOptions.constance_enabled : '';
+export const multiTenancy = window.sysadmin ? window.sysadmin.pageOptions.multi_tenancy : '';
+export const multiInstitution = window.sysadmin ? window.sysadmin.pageOptions.multi_institution : '';
+export const sysadminExtraEnabled = window.sysadmin ? window.sysadmin.pageOptions.sysadmin_extra_enabled : '';
+export const enableGuestInvitation = window.sysadmin ? window.sysadmin.pageOptions.enable_guest_invitation : '';
+export const enableTermsAndConditions = window.sysadmin ? window.sysadmin.pageOptions.enable_terms_and_conditions : '';
+export const isDefaultAdmin = window.sysadmin ? window.sysadmin.pageOptions.is_default_admin : '';
+export const enableFileScan = window.sysadmin ? window.sysadmin.pageOptions.enable_file_scan : '';
+export const canViewSystemInfo = window.sysadmin ? window.sysadmin.pageOptions.admin_permissions.can_view_system_info : '';
+export const canViewStatistic = window.sysadmin ? window.sysadmin.pageOptions.admin_permissions.can_view_statistic : '';
+export const canConfigSystem = window.sysadmin ? window.sysadmin.pageOptions.admin_permissions.can_config_system : '';
+export const canManageLibrary = window.sysadmin ? window.sysadmin.pageOptions.admin_permissions.can_manage_library : '';
+export const canManageUser = window.sysadmin ? window.sysadmin.pageOptions.admin_permissions.can_manage_user : '';
+export const canManageGroup = window.sysadmin ? window.sysadmin.pageOptions.admin_permissions.can_manage_group : '';
+export const canViewUserLog = window.sysadmin ? window.sysadmin.pageOptions.admin_permissions.can_view_user_log : '';
+export const canViewAdminLog = window.sysadmin ? window.sysadmin.pageOptions.admin_permissions.can_view_admin_log : '';
diff --git a/media/css/seahub_react.css b/media/css/seahub_react.css
index e004f95096..a2288f4a7d 100644
--- a/media/css/seahub_react.css
+++ b/media/css/seahub_react.css
@@ -52,6 +52,7 @@
-webkit-font-smoothing: antialiased;
}
+.sf2-icon-histogram:before { content:"\e000"; }
.sf2-icon-wrench:before { content:"\e001"; }
.sf2-icon-clock:before { content:"\e002"; }
.sf2-icon-bell:before { content:"\e003"; }
@@ -68,21 +69,26 @@
.sf2-icon-history:before { content:"\e014"; }
.sf2-icon-cog1:before { content:"\e015"; }
.sf2-icon-trash:before { content:"\e016"; }
+.sf2-icon-security:before { content:"\e017"; }
.sf2-icon-tick:before { content:"\e01e"; }
.sf2-icon-x2:before { content:"\e01f"; }
.sf2-icon-edit:before { content:"\e018"; }
.sf2-icon-caret-down:before { content:"\e01a"; }
+.sf2-icon-cog2:before { content:"\e01b"; }
.sf2-icon-x1:before { content:"\e01d"; }
.sf2-icon-minus:before {content:"\e01c"}
.sf2-icon-confirm:before {content:"\e01e"}
.sf2-icon-cancel:before {content:"\e01f"}
.sf2-icon-user2:before { content:"\e020"; }
+.sf2-icon-msgs:before { content:"\e021"; }
.sf2-icon-grid-view:before { content:"\e025"; }
.sf2-icon-list-view:before { content:"\e026"; }
.sf2-icon-plus:before { content: "\e027"; }
.sf2-icon-copy:before {content:"\e028"}
.sf2-icon-move:before {content:"\e029"}
.sf2-icon-reply:before { content:"\e02a"; }
+.sf2-icon-admin-log:before { content:"\e02e"; }
+.sf2-icon-info:before { content:"\e02f"; }
.sf2-icon-menu:before { content: "\e031"; }
.sf2-icon-more:before { content: "\e032"; }
.sf2-icon-x3:before {content:"\e035";}
diff --git a/seahub/api2/endpoints/admin/file_scan_records.py b/seahub/api2/endpoints/admin/file_scan_records.py
new file mode 100644
index 0000000000..9ee03d3b55
--- /dev/null
+++ b/seahub/api2/endpoints/admin/file_scan_records.py
@@ -0,0 +1,64 @@
+# Copyright (c) 2012-2016 Seafile Ltd.
+import logging
+import json
+
+from rest_framework.authentication import SessionAuthentication
+from rest_framework.permissions import IsAdminUser
+from rest_framework.response import Response
+from rest_framework.views import APIView
+from rest_framework import status
+
+from seahub.api2.authentication import TokenAuthentication
+from seahub.api2.throttling import UserRateThrottle
+from seahub.api2.permissions import IsProVersion
+from seahub.api2.utils import api_error
+from seahub.utils import get_file_scan_record
+
+from seaserv import seafile_api
+
+logger = logging.getLogger(__name__)
+
+
+class AdminFileScanRecords(APIView):
+
+ authentication_classes = (TokenAuthentication, SessionAuthentication)
+ permission_classes = (IsAdminUser, IsProVersion)
+ throttle_classes = (UserRateThrottle,)
+
+ def get(self, request):
+ """get file content scan records
+ """
+ try:
+ page = int(request.GET.get('page', ''))
+ except ValueError:
+ page = 1
+
+ try:
+ per_page = int(request.GET.get('per_page', ''))
+ except ValueError:
+ per_page = 25
+
+ start = (page - 1) * per_page
+ count = per_page
+
+ try:
+ record_list = get_file_scan_record(start, count)
+ except Exception as e:
+ logger.error(e)
+ error_msg = 'Internal Server Error'
+ return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
+
+ for record in record_list:
+ repo = seafile_api.get_repo(record["repo_id"])
+ if not repo:
+ record["repo_name"] = ""
+ else:
+ record["repo_name"] = repo.name
+ record_detail = json.loads(record['detail'])
+ detail_dict = record_detail.values()[0]
+ detail = dict()
+ detail["suggestion"] = detail_dict["suggestion"]
+ detail["label"] = detail_dict["label"]
+ record["detail"] = detail
+
+ return Response({"record_list": record_list}, status=status.HTTP_200_OK)
diff --git a/seahub/base/context_processors.py b/seahub/base/context_processors.py
index afc8fa88dd..8197284353 100644
--- a/seahub/base/context_processors.py
+++ b/seahub/base/context_processors.py
@@ -44,6 +44,11 @@ try:
from seahub.settings import MULTI_TENANCY
except ImportError:
MULTI_TENANCY = False
+try:
+ from seahub.settings import ENABLE_FILE_SCAN
+except ImportError:
+ ENABLE_FILE_SCAN = False
+
def base(request):
"""
@@ -120,6 +125,7 @@ def base(request):
'enable_upload_folder': dj_settings.ENABLE_UPLOAD_FOLDER,
'enable_resumable_fileupload': dj_settings.ENABLE_RESUMABLE_FILEUPLOAD,
'service_url': get_service_url().rstrip('/'),
+ 'enable_file_scan': ENABLE_FILE_SCAN,
}
if request.user.is_staff:
diff --git a/seahub/settings.py b/seahub/settings.py
index 238d0cd50c..acec70729f 100644
--- a/seahub/settings.py
+++ b/seahub/settings.py
@@ -258,6 +258,9 @@ INSTALLED_APPS = (
'seahub.related_files',
)
+# Enable or disable view File Scan
+# ENABLE_FILE_SCAN = True
+
# Enable or disable multiple storage backends.
ENABLE_STORAGE_CLASSES = False
diff --git a/seahub/templates/sysadmin/base.html b/seahub/templates/sysadmin/base.html
index 0679c1aaed..b85162d9d1 100644
--- a/seahub/templates/sysadmin/base.html
+++ b/seahub/templates/sysadmin/base.html
@@ -94,6 +94,12 @@
{% endif %}
+ {% if is_pro and is_default_admin and enable_file_scan %}
+
+ {% trans "File Scan" %}
+
+ {% endif %}
+
{% if is_pro and is_default_admin %}
{% trans "Virus Scan" %}
diff --git a/seahub/templates/sysadmin/sys_file_scan_records_react.html b/seahub/templates/sysadmin/sys_file_scan_records_react.html
new file mode 100644
index 0000000000..ee472d4eea
--- /dev/null
+++ b/seahub/templates/sysadmin/sys_file_scan_records_react.html
@@ -0,0 +1,33 @@
+{% extends "base_for_react.html" %}
+{% load seahub_tags i18n %}
+{% load render_bundle from webpack_loader %}
+
+{% block extra_script %}
+
+{% render_bundle 'sysAdmin' %}
+{% endblock %}
diff --git a/seahub/urls.py b/seahub/urls.py
index ed7036930d..c686b1933b 100644
--- a/seahub/urls.py
+++ b/seahub/urls.py
@@ -130,6 +130,7 @@ from seahub.api2.endpoints.admin.address_book.groups import AdminAddressBookGrou
AdminAddressBookGroup
from seahub.api2.endpoints.admin.group_owned_libraries import AdminGroupOwnedLibraries, \
AdminGroupOwnedLibrary
+from seahub.api2.endpoints.admin.file_scan_records import AdminFileScanRecords
urlpatterns = [
url(r'^accounts/', include('seahub.base.registration_urls')),
@@ -501,6 +502,9 @@ urlpatterns = [
url(r'^api/v2.1/admin/address-book/groups/$', AdminAddressBookGroups.as_view(), name='api-v2.1-admin-address-book-groups'),
url(r'^api/v2.1/admin/address-book/groups/(?P\d+)/$', AdminAddressBookGroup.as_view(), name='api-v2.1-admin-address-book-group'),
+ ## admin::file-scan-records
+ url(r'^api/v2.1/admin/file-scan-records/$', AdminFileScanRecords.as_view(), name='api-v2.1-admin-file-scan-records'),
+
### system admin ###
url(r'^sysadmin/$', sysadmin, name='sysadmin'),
url(r'^sys/settings/$', sys_settings, name='sys_settings'),
@@ -572,6 +576,15 @@ urlpatterns = [
url(r'^client-login/$', client_token_login, name='client_token_login'),
]
+try:
+ from seahub.settings import ENABLE_FILE_SCAN
+except ImportError:
+ ENABLE_FILE_SCAN = False
+if ENABLE_FILE_SCAN:
+ urlpatterns += [
+ url(r'^sys/file-scan-records/$', sys_file_scan_records, name="sys_file_scan_records"),
+ ]
+
from seahub.utils import EVENTS_ENABLED
if EVENTS_ENABLED:
urlpatterns += [
diff --git a/seahub/utils/__init__.py b/seahub/utils/__init__.py
index be950275d2..7cef763780 100644
--- a/seahub/utils/__init__.py
+++ b/seahub/utils/__init__.py
@@ -829,6 +829,11 @@ if EVENTS_CONFIG_FILE:
def get_virus_record_by_id(vid):
with _get_seafevents_session() as session:
return seafevents.get_virus_record_by_id(session, vid)
+
+ def get_file_scan_record(start=-1, limit=-1):
+ records = seafevents_api.get_content_scan_results(start, limit)
+ return records if records else []
+
else:
EVENTS_ENABLED = False
def get_user_events():
@@ -867,6 +872,8 @@ else:
pass
def get_virus_record_by_id(vid):
pass
+ def get_file_scan_record():
+ pass
def calc_file_path_hash(path, bits=12):
if isinstance(path, unicode):
diff --git a/seahub/views/sysadmin.py b/seahub/views/sysadmin.py
index eb4afc5936..321bfd8a46 100644
--- a/seahub/views/sysadmin.py
+++ b/seahub/views/sysadmin.py
@@ -88,8 +88,16 @@ try:
from seahub_extra.organizations.models import OrgSettings
except ImportError:
MULTI_TENANCY = False
+try:
+ from seahub.settings import ENABLE_SYSADMIN_EXTRA
+except ImportError:
+ ENABLE_SYSADMIN_EXTRA = False
from seahub.utils.two_factor_auth import has_two_factor_auth
from termsandconditions.models import TermsAndConditions
+try:
+ from seahub.settings import ENABLE_FILE_SCAN
+except ImportError:
+ ENABLE_FILE_SCAN = False
logger = logging.getLogger(__name__)
@@ -124,6 +132,22 @@ def sysadmin(request):
'trash_repos_expire_days': expire_days if expire_days > 0 else 30,
})
+
+@login_required
+@sys_staff_required
+def sys_file_scan_records(request):
+
+ return render(request, 'sysadmin/sys_file_scan_records_react.html', {
+ 'constance_enabled': dj_settings.CONSTANCE_ENABLED,
+ 'multi_tenancy': MULTI_TENANCY,
+ 'multi_institution': getattr(dj_settings, 'MULTI_INSTITUTION', False),
+ 'sysadmin_extra_enabled': ENABLE_SYSADMIN_EXTRA,
+ 'enable_guest_invitation': ENABLE_GUEST_INVITATION,
+ 'enable_terms_and_conditions': config.ENABLE_TERMS_AND_CONDITIONS,
+ 'enable_file_scan': ENABLE_FILE_SCAN,
+ })
+
+
@login_required
@sys_staff_required
def sys_statistic_file(request):