diff --git a/frontend/src/components/dialog/add-abuse-report-dialog.js b/frontend/src/components/dialog/add-abuse-report-dialog.js new file mode 100644 index 0000000000..2fd194d63a --- /dev/null +++ b/frontend/src/components/dialog/add-abuse-report-dialog.js @@ -0,0 +1,102 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Button, Form, FormGroup, Label, Input, Modal, ModalHeader, ModalBody, ModalFooter, Alert } from 'reactstrap'; +import { gettext } from '../../utils/constants'; +import { seafileAPI } from '../../utils/seafile-api'; +import toaster from '../toast'; + +const propTypes = { + sharedToken: PropTypes.string.isRequired, + filePath: PropTypes.string.isRequired, + toggleAddAbuseReportDialog: PropTypes.func.isRequired, + isAddAbuseReportDialogOpen: PropTypes.bool.isRequired, + contactEmail: PropTypes.string.isRequired, +}; + +class AddAbuseReportDialog extends React.Component { + + constructor(props) { + super(props); + this.state = { + abuseType: 'copyright', + description: '', + reporter: this.props.contactEmail, + errMessage: '', + }; + } + + onAbuseReport = () => { + if (!this.state.reporter) { + this.setState({ + errMessage: gettext('Contact information is required.') + }); + return; + } + seafileAPI.addAbuseReport(this.props.sharedToken, this.state.abuseType, this.state.description, this.state.reporter, this.props.filePath).then((res) => { + this.props.toggleAddAbuseReportDialog(); + toaster.success(gettext('Success'), {duration: 2}); + }).catch((error) => { + if (error.response) { + this.setState({ + errMessage: error.response.data.error_msg + }); + } + }); + }; + + onAbuseTypeChange = (event) => { + let type = event.target.value; + if (type === this.state.abuseType) { + return; + } + this.setState({abuseType: type}); + }; + + setReporter = (event) => { + let reporter = event.target.value.trim(); + this.setState({reporter: reporter}); + }; + + setDescription = (event) => { + let desc = event.target.value.trim(); + this.setState({description: desc}); + }; + + render() { + return ( + + {gettext('Report Abuse')} + +
+ + + this.onAbuseTypeChange(event)}> + + + + + + + + + this.setReporter(event)}/> + + + + this.setDescription(event)}/> + +
+ {this.state.errMessage && {this.state.errMessage}} +
+ + + + +
+ ); + } +} + +AddAbuseReportDialog.propTypes = propTypes; + +export default AddAbuseReportDialog; diff --git a/frontend/src/components/shared-file-view/shared-file-view.js b/frontend/src/components/shared-file-view/shared-file-view.js index fc4b61cc05..791fd09a4d 100644 --- a/frontend/src/components/shared-file-view/shared-file-view.js +++ b/frontend/src/components/shared-file-view/shared-file-view.js @@ -5,6 +5,7 @@ import { gettext, siteRoot, mediaUrl, logoPath, logoWidth, logoHeight, siteTitle import { Button } from 'reactstrap'; import { Utils } from '../../utils/utils'; import SaveSharedFileDialog from '../dialog/save-shared-file-dialog'; +import AddAbuseReportDialog from '../../components/dialog/add-abuse-report-dialog'; import toaster from '../toast'; import watermark from 'watermark-dom'; @@ -15,14 +16,16 @@ const propTypes = { }; let loginUser = window.app.pageOptions.name; -const { repoID, sharedToken, trafficOverLimit, fileName, fileSize, sharedBy, siteName, enableWatermark, canDownload, zipped, filePath } = window.shared.pageOptions; +let contactEmail = window.app.pageOptions.contactEmail; +const { repoID, sharedToken, trafficOverLimit, fileName, fileSize, sharedBy, siteName, enableWatermark, canDownload, zipped, filePath, enableShareLinkReportAbuse } = window.shared.pageOptions; class SharedFileView extends React.Component { constructor(props) { super(props); this.state = { - showSaveSharedFileDialog: false + showSaveSharedFileDialog: false, + isAddAbuseReportDialogOpen: false }; } @@ -44,6 +47,12 @@ class SharedFileView extends React.Component { }); } + toggleAddAbuseReportDialog = () => { + this.setState({ + isAddAbuseReportDialogOpen: !this.state.isAddAbuseReportDialogOpen + }); + } + componentDidMount() { if (trafficOverLimit) { toaster.danger(gettext('File download is disabled: the share link traffic of owner is used up.'), { @@ -106,18 +115,21 @@ class SharedFileView extends React.Component {

{gettext('Shared by:')}{' '}{sharedBy}

} - {canDownload && -
- {(loginUser && loginUser !== sharedBy) && - - }{' '} - {!trafficOverLimit && +
+ {(canDownload && loginUser && (loginUser !== sharedBy)) && + + }{' '} + {(canDownload && !trafficOverLimit) && {gettext('Download')}({Utils.bytesToSize(fileSize)}) - } -
- } + }{' '} + {(enableShareLinkReportAbuse && (loginUser !== sharedBy)) && + + } +
{this.props.content} @@ -129,6 +141,15 @@ class SharedFileView extends React.Component { handleSaveSharedFile={this.handleSaveSharedFile} /> } + {(this.state.isAddAbuseReportDialogOpen && enableShareLinkReportAbuse) && + + } ); } diff --git a/frontend/src/pages/sys-admin/abuse-reports.js b/frontend/src/pages/sys-admin/abuse-reports.js new file mode 100644 index 0000000000..3e65c563a0 --- /dev/null +++ b/frontend/src/pages/sys-admin/abuse-reports.js @@ -0,0 +1,124 @@ +import React, { Component, Fragment } from 'react'; +import Account from '../../components/common/account'; +import { gettext, siteRoot, mediaUrl } from '../../utils/constants'; +import { Utils } from '../../utils/utils'; +import { seafileAPI } from '../../utils/seafile-api'; +import toaster from '../../components/toast'; +import moment from 'moment'; + +class AbuseReports extends Component { + + constructor(props) { + super(props); + this.state = { + abuseReportList: [], + }; + } + + listAbuseReports = () => { + seafileAPI.sysAdminListAbuseReports().then((res) => { + this.setState({ + abuseReportList: res.data.abuse_report_list, + }); + }).catch((error) => { + this.handleError(error); + }); + }; + + updateAbuseReport = (handled, abuseReportId) => { + seafileAPI.sysAdminUpdateAbuseReport(handled, abuseReportId).then((res) => { + const abuseReportList = this.state.abuseReportList.map((item, index) => { + if (item.id === abuseReportId) { + item.handled = res.data.handled; + } + return item; + }); + this.setState({ + abuseReportList: abuseReportList, + }); + }).catch((error) => { + this.handleError(error); + }); + }; + + handleError = (e) => { + if (e.response) { + toaster.danger(e.response.data.error_msg || e.response.data.detail || gettext('Error'), {duration: 3}); + } else { + toaster.danger(gettext('Please check the network.'), {duration: 3}); + } + }; + + componentDidMount() { + this.listAbuseReports(); + } + + render() { + const isDesktop = Utils.isDesktop(); + const AbuseReportList = this.state.abuseReportList.map((item, index) => { + const handled = (!item.handled).toString(); + const abuseReportId = item.id; + const fileUrl = siteRoot + 'lib/' + item.repo_id + '/file' + item.file_path; + return ( + + {item.repo_name} + {item.file_path} + {item.reporter} + {item.abuse_type} + {item.description} + {moment(item.time).format('YYYY-MM-DD')} +

{gettext(item.handled.toString())}

+ + ); + }); + + return ( + +
+
+ +
+
+ +
+
+
+
+
+

{gettext('Abuse Reports')}

+
+
+ + + {isDesktop ? + + + + + + + + + + : + + + + + } + + + {AbuseReportList} + +
{gettext("Library")}{gettext("File")}{gettext("Reporter")}{gettext("Abuse Type")}{gettext("Description")}{gettext("Time")}{gettext("Handled")}
+
+
+
+
+ ); + } +} + +export default AbuseReports; \ No newline at end of file diff --git a/frontend/src/pages/sys-admin/index.js b/frontend/src/pages/sys-admin/index.js index 5e8d8f77f3..549b2ca90f 100644 --- a/frontend/src/pages/sys-admin/index.js +++ b/frontend/src/pages/sys-admin/index.js @@ -10,6 +10,7 @@ import Info from './info'; import DesktopDevices from './devices/desktop-devices'; import MobileDevices from './devices/mobile-devices'; import DeviceErrors from './devices/devices-errors'; +import AbuseReports from './abuse-reports'; import Users from './users/users'; import AdminUsers from './users/admin-users'; @@ -202,6 +203,7 @@ class SysAdmin extends React.Component { currentTab={currentTab} tabItemClick={this.tabItemClick} /> + diff --git a/frontend/src/pages/sys-admin/side-panel.js b/frontend/src/pages/sys-admin/side-panel.js index c9b42e3dbc..fb7577afbd 100644 --- a/frontend/src/pages/sys-admin/side-panel.js +++ b/frontend/src/pages/sys-admin/side-panel.js @@ -5,7 +5,8 @@ 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, enableWorkWeixin } from '../../utils/constants'; + enableGuestInvitation, enableTermsAndConditions, enableFileScan, enableWorkWeixin, + enableShareLinkReportAbuse } from '../../utils/constants'; const propTypes = { isSidePanelClosed: PropTypes.bool.isRequired, @@ -247,6 +248,18 @@ class SidePanel extends React.Component { } + {isDefaultAdmin && enableShareLinkReportAbuse && +
  • + this.props.tabItemClick('abuse-reports')} + > + + {gettext('Abuse Reports')} + +
  • + } diff --git a/frontend/src/shared-file-view-markdown.js b/frontend/src/shared-file-view-markdown.js index 3e56e25b67..2eb5d33440 100644 --- a/frontend/src/shared-file-view-markdown.js +++ b/frontend/src/shared-file-view-markdown.js @@ -44,8 +44,8 @@ class FileContent extends React.Component { let imageUrl = innerNode.data.src; const re = new RegExp(serviceURL + '/lib/' + repoID +'/file.*raw=1'); - - // different repo + + // different repo if (!re.test(imageUrl)) { return; } @@ -78,7 +78,7 @@ class FileContent extends React.Component { return (
    - , document.getElementById('wrapper') -); +); \ No newline at end of file diff --git a/frontend/src/utils/constants.js b/frontend/src/utils/constants.js index 861b1b995f..d169ea064e 100644 --- a/frontend/src/utils/constants.js +++ b/frontend/src/utils/constants.js @@ -126,3 +126,4 @@ export const canViewAdminLog = window.sysadmin ? window.sysadmin.pageOptions.adm export const enableWorkWeixin = window.sysadmin ? window.sysadmin.pageOptions.enable_work_weixin : ''; export const enableSysAdminViewRepo = window.sysadmin ? window.sysadmin.pageOptions.enableSysAdminViewRepo : ''; export const haveLDAP = window.sysadmin ? window.sysadmin.pageOptions.haveLDAP : ''; +export const enableShareLinkReportAbuse = window.sysadmin ? window.sysadmin.pageOptions.enable_share_link_report_abuse : ''; diff --git a/seahub/abuse_reports/__init__.py b/seahub/abuse_reports/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/seahub/abuse_reports/migrations/0001_initial.py b/seahub/abuse_reports/migrations/0001_initial.py new file mode 100644 index 0000000000..f8ae92ff81 --- /dev/null +++ b/seahub/abuse_reports/migrations/0001_initial.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.23 on 2019-11-05 02:41 +from __future__ import unicode_literals + +import datetime +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='AbuseReport', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('reporter', models.TextField(blank=True, null=True)), + ('repo_id', models.CharField(max_length=36)), + ('repo_name', models.CharField(max_length=255)), + ('file_path', models.TextField(blank=True, null=True)), + ('abuse_type', models.CharField(choices=[('copyright', 'copyright'), ('virus', 'virus'), ('abuse_content', 'abuse_content'), ('other', 'other')], db_index=True, max_length=255)), + ('description', models.TextField(blank=True, null=True)), + ('handled', models.BooleanField(db_index=True, default=False)), + ('time', models.DateTimeField(default=datetime.datetime.now)), + ], + options={ + 'ordering': ['-time'], + }, + ), + ] diff --git a/seahub/abuse_reports/migrations/__init__.py b/seahub/abuse_reports/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/seahub/abuse_reports/models.py b/seahub/abuse_reports/models.py new file mode 100644 index 0000000000..48c6df44c0 --- /dev/null +++ b/seahub/abuse_reports/models.py @@ -0,0 +1,71 @@ +# Copyright (c) 2012-2017 Seafile Ltd. + +import datetime + +from django.db import models +from django.conf import settings + +COPYRIGHT_ISSUE = 'copyright' +VIRUS_ISSUE = 'virus' +ABUSE_CONTENT_ISSUE = 'abuse_content' +OTHER_ISSUE = 'other' + + +class AbuseReportManager(models.Manager): + + def add_abuse_report(self, reporter, repo_id, repo_name, file_path, + abuse_type, description=None): + + model = super(AbuseReportManager, self).create( + reporter=reporter, repo_id=repo_id, repo_name=repo_name, + file_path=file_path, abuse_type=abuse_type, + description=description) + + model.save() + + return model + + def get_abuse_report_by_id(self, pk): + + try: + report = super(AbuseReportManager, self).get(id=pk) + except AbuseReport.DoesNotExist: + return None + + return report + + def get_abuse_reports(self, abuse_type=None, handled=None): + + reports = super(AbuseReportManager, self).all() + + if abuse_type: + reports = reports.filter(abuse_type=abuse_type) + + if handled in (True, False): + reports = reports.filter(handled=handled) + + return reports + + +class AbuseReport(models.Model): + ABUSE_TYPE_CHOICES = ( + (COPYRIGHT_ISSUE, 'copyright'), + (VIRUS_ISSUE, 'virus'), + (ABUSE_CONTENT_ISSUE, 'abuse_content'), + (OTHER_ISSUE, 'other'), + ) + + reporter = models.TextField(blank=True, null=True) + repo_id = models.CharField(max_length=36) + repo_name = models.CharField(max_length=settings.MAX_FILE_NAME) + file_path = models.TextField(blank=True, null=True) + abuse_type = models.CharField(max_length=255, + db_index=True, choices=ABUSE_TYPE_CHOICES, ) + description = models.TextField(blank=True, null=True) + handled = models.BooleanField(default=False, db_index=True) + time = models.DateTimeField(default=datetime.datetime.now) + + objects = AbuseReportManager() + + class Meta: + ordering = ["-time"] diff --git a/seahub/api2/endpoints/abuse_reports.py b/seahub/api2/endpoints/abuse_reports.py new file mode 100644 index 0000000000..431bd17f06 --- /dev/null +++ b/seahub/api2/endpoints/abuse_reports.py @@ -0,0 +1,119 @@ +# Copyright (c) 2012-2016 Seafile Ltd. +import os +import logging +import posixpath + +from rest_framework.authentication import SessionAuthentication +from rest_framework.response import Response +from rest_framework.views import APIView +from rest_framework import status + +from seaserv import seafile_api + +from seahub.api2.utils import api_error +from seahub.api2.authentication import TokenAuthentication +from seahub.api2.throttling import AnonRateThrottle +from seahub.share.models import FileShare +from seahub.utils import normalize_file_path +from seahub.utils.timeutils import datetime_to_isoformat_timestr +from seahub.settings import ENABLE_SHARE_LINK_REPORT_ABUSE + +from seahub.abuse_reports.models import COPYRIGHT_ISSUE, \ + VIRUS_ISSUE, ABUSE_CONTENT_ISSUE, OTHER_ISSUE, AbuseReport + +logger = logging.getLogger(__name__) + + +def get_abuse_report_info(report): + data = {} + + file_path = report.file_path + + data['id'] = report.id + data['reporter'] = report.reporter + data['repo_id'] = report.repo_id + data['repo_name'] = report.repo_name + data['file_path'] = file_path + data['file_name'] = os.path.basename(file_path) + data['time'] = datetime_to_isoformat_timestr(report.time) + data['abuse_type'] = report.abuse_type + data['description'] = report.description + data['handled'] = report.handled + + return data + + +class AbuseReportsView(APIView): + authentication_classes = (TokenAuthentication, SessionAuthentication) + throttle_classes = (AnonRateThrottle,) + + def post(self, request): + """ Create abuse report. + + Permission checking: + 1. all user; + """ + + if not ENABLE_SHARE_LINK_REPORT_ABUSE: + error_msg = 'Feature not enabled.' + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + + # argument check + share_link_token = request.data.get('share_link_token', '') + if not share_link_token: + error_msg = 'share_link_token invalid.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + abuse_type = request.data.get('abuse_type', '') + if not abuse_type: + error_msg = 'abuse_type invalid.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + if abuse_type not in (COPYRIGHT_ISSUE, VIRUS_ISSUE, + ABUSE_CONTENT_ISSUE, OTHER_ISSUE): + error_msg = 'abuse_type invalid.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + reporter = request.data.get('reporter', '') + description = request.data.get('description', '') + + # resource check + share_link = FileShare.objects.get_valid_file_link_by_token(share_link_token) + if not share_link: + error_msg = 'Share link %s not found.' % share_link_token + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + repo_id = share_link.repo_id + repo = seafile_api.get_repo(repo_id) + if not repo: + error_msg = 'Library %s not found.' % repo_id + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + file_path = share_link.path + file_path = normalize_file_path(file_path) + file_id = seafile_api.get_file_id_by_path(repo_id, file_path) + + if not file_id: + # view file via shared dir + req_path = request.data.get('file_path', '') + if not req_path: + file_id = None + else: + dir_path = normalize_file_path(share_link.path) + file_path = posixpath.join(dir_path, normalize_file_path(req_path).lstrip('/')) + file_id = seafile_api.get_file_id_by_path(repo_id, file_path) + + if not file_id: + error_msg = 'File %s not found.' % file_path + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + try: + report = AbuseReport.objects.add_abuse_report( + reporter, repo_id, repo.repo_name, file_path, abuse_type, description) + except Exception as e: + logger.error(e) + error_msg = 'Internal Server Error' + return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) + + info = get_abuse_report_info(report) + return Response(info) diff --git a/seahub/api2/endpoints/admin/abuse_reports.py b/seahub/api2/endpoints/admin/abuse_reports.py new file mode 100644 index 0000000000..1c2fe25079 --- /dev/null +++ b/seahub/api2/endpoints/admin/abuse_reports.py @@ -0,0 +1,99 @@ +# Copyright (c) 2012-2016 Seafile Ltd. +import logging + +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.utils import api_error, to_python_boolean + +from seahub.api2.endpoints.abuse_reports import get_abuse_report_info +from seahub.abuse_reports.models import AbuseReport +from seahub.settings import ENABLE_SHARE_LINK_REPORT_ABUSE + +logger = logging.getLogger(__name__) + + +class AdminAbuseReportsView(APIView): + authentication_classes = (TokenAuthentication, SessionAuthentication) + permission_classes = (IsAdminUser,) + throttle_classes = (UserRateThrottle,) + + def get(self, request): + """ Get all abuse reports. + + Permission checking: + 1. only admin can perform this action. + """ + + if not ENABLE_SHARE_LINK_REPORT_ABUSE: + error_msg = 'Feature not enabled.' + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + + abuse_type = request.GET.get('abuse_type', '') + handled = request.GET.get('handled', '') + + if handled: + if handled not in ('true', 'false'): + error_msg = 'handled invalid.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + handled = to_python_boolean(handled) + + try: + reports = AbuseReport.objects.get_abuse_reports( + abuse_type=abuse_type, handled=handled) + except Exception as e: + logger.error(e) + error_msg = 'Internal Server Error' + return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) + + info_list = [] + for report in reports: + info = get_abuse_report_info(report) + info_list.append(info) + + return Response({'abuse_report_list': info_list, }) + + +class AdminAbuseReportView(APIView): + authentication_classes = (TokenAuthentication, SessionAuthentication) + permission_classes = (IsAdminUser,) + throttle_classes = (UserRateThrottle,) + + def put(self, request, pk): + """ Mark an abuse report handled. + + Permission checking: + 1. only admin can perform this action. + """ + + if not ENABLE_SHARE_LINK_REPORT_ABUSE: + error_msg = 'Feature not enabled.' + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + + # argument check + handled = request.data.get('handled') + if not handled: + error_msg = 'handled invalid.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + if handled not in ('true', 'false'): + error_msg = 'handled invalid.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + # resource check + report = AbuseReport.objects.get_abuse_report_by_id(pk) + if not report: + error_msg = 'abuse report %d not found.' % pk + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + report.handled = to_python_boolean(handled) + report.save() + + info = get_abuse_report_info(report) + return Response(info) diff --git a/seahub/base/context_processors.py b/seahub/base/context_processors.py index 2bc42e7e20..08cbf1358a 100644 --- a/seahub/base/context_processors.py +++ b/seahub/base/context_processors.py @@ -21,7 +21,7 @@ from seahub.settings import SEAFILE_VERSION, SITE_TITLE, SITE_NAME, \ FAVICON_PATH, ENABLE_THUMBNAIL, THUMBNAIL_SIZE_FOR_ORIGINAL, \ MEDIA_ROOT, SHOW_LOGOUT_ICON, CUSTOM_LOGO_PATH, CUSTOM_FAVICON_PATH, \ ENABLE_SEAFILE_DOCS, LOGIN_BG_IMAGE_PATH, \ - CUSTOM_LOGIN_BG_PATH + CUSTOM_LOGIN_BG_PATH, ENABLE_SHARE_LINK_REPORT_ABUSE from seahub.constants import DEFAULT_ADMIN from seahub.utils import get_site_name, get_service_url @@ -143,6 +143,7 @@ def base(request): if request.user.is_staff: result['is_default_admin'] = request.user.admin_role == DEFAULT_ADMIN + result['enable_share_link_report_abuse'] = ENABLE_SHARE_LINK_REPORT_ABUSE return result diff --git a/seahub/settings.py b/seahub/settings.py index aa58f1c6e4..554b761ceb 100644 --- a/seahub/settings.py +++ b/seahub/settings.py @@ -255,6 +255,7 @@ INSTALLED_APPS = ( 'seahub.work_weixin', 'seahub.file_participants', 'seahub.repo_api_tokens', + 'seahub.abuse_reports', ) # Enable or disable view File Scan @@ -347,6 +348,9 @@ SHARE_LINK_PASSWORD_MIN_LENGTH = 8 # enable or disable share link audit ENABLE_SHARE_LINK_AUDIT = False +# enable or disable report abuse file on share link page +ENABLE_SHARE_LINK_REPORT_ABUSE = False + # share link audit code timeout SHARE_LINK_AUDIT_CODE_TIMEOUT = 60 * 60 diff --git a/seahub/templates/js/sysadmin-templates.html b/seahub/templates/js/sysadmin-templates.html index e1419870c8..fd2e8d88d1 100644 --- a/seahub/templates/js/sysadmin-templates.html +++ b/seahub/templates/js/sysadmin-templates.html @@ -118,12 +118,19 @@ {% endif %} + {% if is_default_admin and enable_work_weixin %}
  • 企业微信集成
  • {% endif %} + {% if is_default_admin and enable_share_link_report_abuse %} +
  • + {% trans "Abuse Reports" %} +
  • + {% endif %} + <% if (cur_tab == 'libraries') { %> <% if (option == 'all') { %> @@ -1024,3 +1031,55 @@ + + + + diff --git a/seahub/templates/shared_file_view_react.html b/seahub/templates/shared_file_view_react.html index 09c5d65d42..847cff4736 100644 --- a/seahub/templates/shared_file_view_react.html +++ b/seahub/templates/shared_file_view_react.html @@ -69,7 +69,8 @@ body { fileType: '{{ filetype }}', fileExt: '{{ fileext }}', commitID: '{{ current_commit.id }}' || '{{ repo.head_cmmt_id }}', - + enableShareLinkReportAbuse: {% if enable_share_link_report_abuse %}true{% else %}false{% endif %}, + // for 'view file in shared dir' {% if zipped %} zipped: (function() { diff --git a/seahub/templates/sysadmin/base.html b/seahub/templates/sysadmin/base.html index d4e681146b..5369952447 100644 --- a/seahub/templates/sysadmin/base.html +++ b/seahub/templates/sysadmin/base.html @@ -135,6 +135,12 @@ {% endif %} + {% if is_default_admin and enable_share_link_report_abuse %} +
  • + {% trans "Abuse Reports" %} +
  • + {% endif %} + {% endblock %}
    diff --git a/seahub/templates/sysadmin/sysadmin_react_app.html b/seahub/templates/sysadmin/sysadmin_react_app.html index 4c6f2de6d5..7453fa4adc 100644 --- a/seahub/templates/sysadmin/sysadmin_react_app.html +++ b/seahub/templates/sysadmin/sysadmin_react_app.html @@ -42,6 +42,7 @@ return list; })(), haveLDAP: {% if have_ldap %} true {% else %} false {% endif %}, + enable_share_link_report_abuse: {% if enable_share_link_report_abuse %} true {% else %} false {% endif %}, admin_permissions: { "can_view_system_info": {% if user.admin_permissions.can_view_system_info %} true {% else %} false {% endif %}, "can_view_statistic": {% if user.admin_permissions.can_view_statistic %} true {% else %} false {% endif %}, diff --git a/seahub/urls.py b/seahub/urls.py index e8f96da2ff..ffda018b4c 100644 --- a/seahub/urls.py +++ b/seahub/urls.py @@ -92,9 +92,10 @@ from seahub.api2.endpoints.public_repos_search import PublishedRepoSearchView from seahub.api2.endpoints.recent_added_files import RecentAddedFilesView from seahub.api2.endpoints.repo_api_tokens import RepoAPITokensView, RepoAPITokenView from seahub.api2.endpoints.via_repo_token import ViaRepoDirView, ViaRepoUploadLinkView - +from seahub.api2.endpoints.abuse_reports import AbuseReportsView # Admin +from seahub.api2.endpoints.admin.abuse_reports import AdminAbuseReportsView, AdminAbuseReportView from seahub.api2.endpoints.admin.revision_tag import AdminTaggedItemsView from seahub.api2.endpoints.admin.login_logs import LoginLogs, AdminLoginLogs from seahub.api2.endpoints.admin.file_audit import FileAudit @@ -446,6 +447,16 @@ urlpatterns = [ # admin: activities url(r'^api/v2.1/admin/user-activities/$', UserActivitiesView.as_view(), name='api-v2.1-admin-user-activity'), + ## user::abuse-report + # user report an abuse file + url(r'^api/v2.1/abuse-reports/$', AbuseReportsView.as_view(), name='api-v2.1-abuse-reports'), + + ## admin::abuse-reports + # admin get all abuse reports + url(r'^api/v2.1/admin/abuse-reports/$', AdminAbuseReportsView.as_view(), name='api-v2.1-admin-abuse-reports'), + url(r'^api/v2.1/admin/abuse-reports/(?P\d+)/$', AdminAbuseReportView.as_view(), name='api-v2.1-admin-abuse-report'), + + ## admin::sysinfo url(r'^api/v2.1/admin/sysinfo/$', SysInfo.as_view(), name='api-v2.1-sysinfo'), @@ -723,6 +734,7 @@ urlpatterns = [ url(r'^sys/work-weixin/$', sysadmin_react_fake_view, name="sys_work_weixin"), url(r'^sys/work-weixin/departments/$', sysadmin_react_fake_view, name="sys_work_weixin_departments"), url(r'^sys/invitations/$', sysadmin_react_fake_view, name="sys_invitations"), + url(r'^sys/abuse-reports/$', sysadmin_react_fake_view, name="sys_abuse_reports"), url(r'^client-login/$', client_token_login, name='client_token_login'), ] diff --git a/seahub/views/file.py b/seahub/views/file.py index ba7305b8a3..04788ecf3b 100644 --- a/seahub/views/file.py +++ b/seahub/views/file.py @@ -88,7 +88,8 @@ import seahub.settings as settings from seahub.settings import FILE_ENCODING_LIST, FILE_PREVIEW_MAX_SIZE, \ FILE_ENCODING_TRY_LIST, MEDIA_URL, SEAFILE_COLLAB_SERVER, ENABLE_WATERMARK, \ SHARE_LINK_EXPIRE_DAYS_MIN, SHARE_LINK_EXPIRE_DAYS_MAX, SHARE_LINK_PASSWORD_MIN_LENGTH, \ - SHARE_LINK_EXPIRE_DAYS_DEFAULT + SHARE_LINK_EXPIRE_DAYS_DEFAULT, ENABLE_SHARE_LINK_REPORT_ABUSE + # wopi try: @@ -1315,7 +1316,8 @@ def view_shared_file(request, fileshare): 'enable_watermark': ENABLE_WATERMARK, 'file_share_link': file_share_link, 'desc_for_ogp': desc_for_ogp, - 'icon_path_for_ogp': icon_path_for_ogp + 'icon_path_for_ogp': icon_path_for_ogp, + 'enable_share_link_report_abuse': ENABLE_SHARE_LINK_REPORT_ABUSE, }) @share_link_audit @@ -1529,6 +1531,7 @@ def view_file_via_shared_dir(request, fileshare): 'file_share_link': file_share_link, 'desc_for_ogp': desc_for_ogp, 'icon_path_for_ogp': icon_path_for_ogp, + 'enable_share_link_report_abuse': ENABLE_SHARE_LINK_REPORT_ABUSE, }) def file_edit_submit(request, repo_id): diff --git a/seahub/views/repo.py b/seahub/views/repo.py index 5148dc54d7..cb85fccb7f 100644 --- a/seahub/views/repo.py +++ b/seahub/views/repo.py @@ -29,7 +29,8 @@ from seahub.settings import ENABLE_UPLOAD_FOLDER, \ ENABLE_RESUMABLE_FILEUPLOAD, ENABLE_THUMBNAIL, \ THUMBNAIL_ROOT, THUMBNAIL_DEFAULT_SIZE, THUMBNAIL_SIZE_FOR_GRID, \ MAX_NUMBER_OF_FILES_FOR_FILEUPLOAD, SHARE_LINK_EXPIRE_DAYS_MIN, \ - SHARE_LINK_EXPIRE_DAYS_MAX, SEAFILE_COLLAB_SERVER + SHARE_LINK_EXPIRE_DAYS_MAX, SEAFILE_COLLAB_SERVER, \ + ENABLE_SHARE_LINK_REPORT_ABUSE from seahub.utils.file_types import IMAGE, VIDEO from seahub.thumbnail.utils import get_share_link_thumbnail_src from seahub.constants import HASH_URLS @@ -341,7 +342,8 @@ def view_shared_dir(request, fileshare): 'mode': mode, 'thumbnail_size': thumbnail_size, 'dir_share_link': dir_share_link, - 'desc_for_ogp': desc_for_ogp + 'desc_for_ogp': desc_for_ogp, + 'enable_share_link_report_abuse': ENABLE_SHARE_LINK_REPORT_ABUSE, }) @share_link_audit diff --git a/seahub/views/sysadmin.py b/seahub/views/sysadmin.py index d6b10a5541..ec4a02fe08 100644 --- a/seahub/views/sysadmin.py +++ b/seahub/views/sysadmin.py @@ -76,7 +76,8 @@ import seahub.settings as settings from seahub.settings import INIT_PASSWD, SITE_ROOT, \ SEND_EMAIL_ON_ADDING_SYSTEM_MEMBER, SEND_EMAIL_ON_RESETTING_USER_PASSWD, \ ENABLE_SYS_ADMIN_VIEW_REPO, ENABLE_GUEST_INVITATION, \ - ENABLE_LIMIT_IPADDRESS + ENABLE_LIMIT_IPADDRESS, ENABLE_SHARE_LINK_REPORT_ABUSE + try: from seahub.settings import ENABLE_TRIAL_ACCOUNT except: @@ -134,6 +135,7 @@ def sysadmin(request): 'trash_repos_expire_days': expire_days if expire_days > 0 else 30, 'enable_file_scan': ENABLE_FILE_SCAN, 'enable_work_weixin': ENABLE_WORK_WEIXIN, + 'enable_share_link_report_abuse': ENABLE_SHARE_LINK_REPORT_ABUSE, }) @login_required @@ -168,6 +170,7 @@ def sysadmin_react_fake_view(request, **kwargs): 'available_admin_roles': get_available_admin_roles(), 'have_ldap': get_ldap_info(), 'two_factor_auth_enabled': has_two_factor_auth(), + 'enable_share_link_report_abuse': ENABLE_SHARE_LINK_REPORT_ABUSE, }) @login_required @@ -393,7 +396,7 @@ def sys_user_admin(request): profile = Profile.objects.get_profile_by_user(user.email) user.institution = profile.institution if profile else '' - return render(request, + return render(request, 'sysadmin/sys_useradmin.html', { 'users': users, 'current_page': current_page, @@ -598,7 +601,7 @@ def sys_user_admin_ldap_imported(request): profile = Profile.objects.get_profile_by_user(user.email) user.institution = profile.institution if profile else '' - return render(request, + return render(request, 'sysadmin/sys_user_admin_ldap_imported.html', { 'users': users, 'current_page': current_page, @@ -649,7 +652,7 @@ def sys_user_admin_ldap(request): if last_login.username == user.email: user.last_login = last_login.last_login - return render(request, + return render(request, 'sysadmin/sys_useradmin_ldap.html', { 'users': users, 'current_page': current_page, @@ -708,7 +711,7 @@ def sys_user_admin_admins(request): extra_admin_roles = [x for x in get_available_admin_roles() if x not in get_basic_admin_roles()] - return render(request, + return render(request, 'sysadmin/sys_useradmin_admins.html', { 'users': admin_users, 'not_admin_users': not_admin_users, @@ -851,7 +854,7 @@ def user_info(request, email): force_2fa = UserOptions.objects.is_force_2fa(user.username) - return render(request, + return render(request, 'sysadmin/userinfo.html', { 'owned_repos': owned_repos, 'space_quota': space_quota, @@ -1446,7 +1449,7 @@ def sys_org_search(request): extra_org_roles = [x for x in get_available_roles() if x != DEFAULT_ORG] - return render(request, + return render(request, 'sysadmin/sys_org_search.html', { 'orgs': orgs, 'name': org_name, @@ -1699,7 +1702,7 @@ def sys_publink_admin(request): else: l.name = os.path.dirname(l.path) - return render(request, + return render(request, 'sysadmin/sys_publink_admin.html', { 'publinks': publinks, 'current_page': current_page, @@ -1740,7 +1743,7 @@ def sys_upload_link_admin(request): else: page_next = False - return render(request, + return render(request, 'sysadmin/sys_upload_link_admin.html', { 'uploadlinks': uploadlinks, 'current_page': current_page, @@ -1804,7 +1807,7 @@ def sys_link_search(request): else: l.name = os.path.dirname(l.path) - return render(request, + return render(request, 'sysadmin/sys_link_search.html', { 'publinks': publinks, 'token': token @@ -1953,7 +1956,7 @@ def sys_virus_scan_records(request): r.repo.owner = seafile_api.get_repo_owner(r.repo.repo_id) records.append(r) - return render(request, + return render(request, 'sysadmin/sys_virus_scan_records.html', { 'records': records, 'current_page': current_page, @@ -2024,7 +2027,7 @@ def batch_add_user_example(request): if not next_page: next_page = SITE_ROOT data_list = [] - head = [_('Email'), _('Password'), _('Name')+ '(' + _('Optional') + ')', + head = [_('Email'), _('Password'), _('Name')+ '(' + _('Optional') + ')', _('Role') + '(' + _('Optional') + ')', _('Space Quota') + '(MB, ' + _('Optional') + ')'] for i in range(5): username = "test" + str(i) +"@example.com" @@ -2183,7 +2186,7 @@ def sys_sudo_mode(request): enable_shib_login = getattr(settings, 'ENABLE_SHIB_LOGIN', False) enable_adfs_login = getattr(settings, 'ENABLE_ADFS_LOGIN', False) - return render(request, + return render(request, 'sysadmin/sudo_mode.html', { 'password_error': password_error, 'enable_sso': enable_shib_login or enable_adfs_login, @@ -2329,7 +2332,7 @@ def sys_inst_admin(request): else: page_next = False - return render(request, + return render(request, 'sysadmin/sys_inst_admin.html', { 'insts': insts[:per_page], 'current_page': current_page, @@ -2616,7 +2619,7 @@ def sys_invitation_admin(request): else: page_next = False - return render(request, + return render(request, 'sysadmin/sys_invitations_admin.html', { 'invitations': invitations, 'current_page': current_page, diff --git a/static/scripts/common.js b/static/scripts/common.js index 228e2cf102..cb189ae07a 100644 --- a/static/scripts/common.js +++ b/static/scripts/common.js @@ -195,6 +195,8 @@ define([ case 'admin-library': return siteRoot + 'api/v2.1/admin/libraries/' + options.repo_id + '/'; case 'admin-library-history-limit': return siteRoot + 'api/v2.1/admin/libraries/' + options.repo_id + '/history-limit/'; case 'admin-library-dirents': return siteRoot + 'api/v2.1/admin/libraries/' + options.repo_id + '/dirents/'; + case 'admin-abuse-reports': return siteRoot + 'api/v2.1/admin/abuse-reports/'; + case 'admin-abuse-report': return siteRoot + 'api/v2.1/admin/abuse-reports/' + options.report_id + '/'; case 'admin-groups': return siteRoot + 'api/v2.1/admin/groups/'; case 'admin-group': return siteRoot + 'api/v2.1/admin/groups/' + options.group_id + '/'; case 'admin-group-libraries': return siteRoot + 'api/v2.1/admin/groups/' + options.group_id + '/libraries/'; diff --git a/static/scripts/sysadmin-app/collection/abuse-reports.js b/static/scripts/sysadmin-app/collection/abuse-reports.js new file mode 100644 index 0000000000..8e71371327 --- /dev/null +++ b/static/scripts/sysadmin-app/collection/abuse-reports.js @@ -0,0 +1,22 @@ +define([ + 'underscore', + 'backbone', + 'common', + 'sysadmin-app/models/abuse-report' +], function(_, Backbone, Common, AbuseReportModel) { + 'use strict'; + + var AbuseReportCollection = Backbone.Collection.extend({ + model: AbuseReportModel, + url: function () { + return Common.getUrl({name: 'admin-abuse-reports'}); + }, + + parse: function(data) { + this.data = data.abuse_report_list; + return data.abuse_report_list; + } + }); + + return AbuseReportCollection; +}); diff --git a/static/scripts/sysadmin-app/models/abuse-report.js b/static/scripts/sysadmin-app/models/abuse-report.js new file mode 100644 index 0000000000..275f89c236 --- /dev/null +++ b/static/scripts/sysadmin-app/models/abuse-report.js @@ -0,0 +1,17 @@ +define([ + 'underscore', + 'backbone', + 'common', +], function(_, Backbone, Common) { + 'use strict'; + + var AbuseReportModel = Backbone.Model.extend({ + + getIconUrl: function(size) { + return Common.getLibIconUrl(false, false, size); + } + + }); + + return AbuseReportModel; +}); diff --git a/static/scripts/sysadmin-app/router.js b/static/scripts/sysadmin-app/router.js index cf8b470016..2fba4639a7 100644 --- a/static/scripts/sysadmin-app/router.js +++ b/static/scripts/sysadmin-app/router.js @@ -27,15 +27,15 @@ define([ 'sysadmin-app/views/admin-operation-logs', 'sysadmin-app/views/admin-login-logs', 'sysadmin-app/views/device-trusted-ipaddresses', + 'sysadmin-app/views/abuse-reports', 'app/views/account' ], function($, Backbone, Common, SideNavView, DashboardView, DesktopDevicesView, MobileDevicesView, DeviceErrorsView, ReposView, SearchReposView, SystemReposView, TrashReposView, - SearchTrashReposView, DirView, - AddressBookView, AddressBookGroupView, + SearchTrashReposView, DirView, AddressBookView, AddressBookGroupView, GroupsView, SearchGroupsView, GroupReposView, GroupMembersView, AdminOperationLogsview, AdminLoginLogsView, DeviceTrustedIPView, - AccountView) { + AbuseReportsView, AccountView) { "use strict"; @@ -47,6 +47,7 @@ define([ 'mobile-devices/': 'showMobileDevices', 'device-errors/': 'showDeviceErrors', 'device-trusted-ip/': 'showDeviceTrustedIP', + 'abuse-reports/': 'showAbuseReports', 'all-libs/': 'showLibraries', 'search-libs/': 'showSearchLibraries', @@ -86,6 +87,8 @@ define([ this.deviceErrorsView = new DeviceErrorsView(); this.deviceTrustedIPView = new DeviceTrustedIPView(); + this.abuseReportsView = new AbuseReportsView(); + this.reposView = new ReposView(); this.searchReposView = new SearchReposView(); this.systemReposView = new SystemReposView(); @@ -386,6 +389,12 @@ define([ this.switchCurrentView(this.adminLoginLogsView); this.sideNavView.setCurTab('admin-logs'); this.adminLoginLogsView.show({'current_page': current_page}); + }, + + showAbuseReports: function() { + this.switchCurrentView(this.abuseReportsView); + this.sideNavView.setCurTab('abuse-reports'); + this.abuseReportsView.show(); } }); diff --git a/static/scripts/sysadmin-app/views/abuse-report.js b/static/scripts/sysadmin-app/views/abuse-report.js new file mode 100644 index 0000000000..9c3b4641fe --- /dev/null +++ b/static/scripts/sysadmin-app/views/abuse-report.js @@ -0,0 +1,89 @@ +define([ + 'jquery', + 'underscore', + 'backbone', + 'common', + 'moment', + 'app/views/widgets/hl-item-view' +], function($, _, Backbone, Common, Moment, HLItemView) { + 'use strict'; + + var AbuseReportView = HLItemView.extend({ + tagName: 'tr', + + template: _.template($('#abuse-report-item-tmpl').html()), + + events: { + 'click .abuse-report-handle': 'handleReport' + }, + + initialize: function() { + HLItemView.prototype.initialize.call(this); + this.listenTo(this.model, "change", this.render); + }, + + render: function() { + var data = this.model.toJSON(), abuse_type_output, + icon_size = Common.isHiDPI() ? 48 : 24, + icon_url = this.model.getIconUrl(icon_size), + time = Moment(data['time']); + + if (data['abuse_type'] == 'copyright') { + abuse_type_output = gettext('Copyright infringement'); + } else if (data['abuse_type'] == 'virus') { + abuse_type_output = gettext('Virus'); + } else if (data['abuse_type'] == 'abuse_content') { + abuse_type_output = gettext('Abuse content'); + } else if (data['abuse_type'] == 'other') { + abuse_type_output = gettext('Other'); + } + + data['time'] = time.format('LLLL'); + data['time_from_now'] = Common.getRelativeTimeStr(time); + data['icon_url'] = icon_url; + data['abuse_type_output'] = abuse_type_output; + + this.$el.html(this.template(data)); + + return this; + }, + + handleReport: function() { + var _this = this, handled_parameter, + report_id = this.model.get('id'), + handled = this.model.get('handled'); + + if (handled) { + handled_parameter = false; + } else { + handled_parameter = true; + } + + $.ajax({ + url: Common.getUrl({ + 'name':'admin-abuse-report', + 'report_id': report_id + }), + type: 'PUT', + cache: false, + beforeSend: Common.prepareCSRFToken, + dataType: 'json', + data: { + 'handled': handled_parameter + }, + success: function() { + _this.model.set({'handled': handled_parameter}); + Common.feedback(gettext("Successfully change the report's status."), 'success'); + }, + error: function(xhr, textStatus, errorThrown) { + Common.ajaxErrorHandler(xhr, textStatus, errorThrown); + } + }); + + return false; + + + } + }); + return AbuseReportView; +}); diff --git a/static/scripts/sysadmin-app/views/abuse-reports.js b/static/scripts/sysadmin-app/views/abuse-reports.js new file mode 100644 index 0000000000..2e620a6910 --- /dev/null +++ b/static/scripts/sysadmin-app/views/abuse-reports.js @@ -0,0 +1,87 @@ +define([ + 'jquery', + 'underscore', + 'backbone', + 'common', + 'sysadmin-app/views/abuse-report', + 'sysadmin-app/collection/abuse-reports' +], function($, _, Backbone, Common, AbuseReportView, AbuseReportsCollection) { + 'use strict'; + + var AbuseReportsView = Backbone.View.extend({ + + id: 'admin-abuse-reports', + + template: _.template($("#abuse-reports-tmpl").html()), + + initialize: function() { + this.abuseReportsCollection = new AbuseReportsCollection(); + this.listenTo(this.abuseReportsCollection, 'add', this.addOne); + this.listenTo(this.abuseReportsCollection, 'reset', this.reset); + this.render(); + }, + + render: function() { + this.$el.html(this.template()); + + this.$table = this.$('table'); + this.$tableBody = $('tbody', this.$table); + this.$loadingTip = this.$('.loading-tip'); + this.$emptyTip = this.$('.empty-tips'); + this.$error = this.$('.error'); + }, + + initPage: function() { + this.$loadingTip.show(); + this.$table.hide(); + this.$tableBody.empty(); + this.$emptyTip.hide(); + this.$error.hide(); + }, + + hide: function() { + this.$el.detach(); + this.attached = false; + }, + + show: function() { + if (!this.attached) { + this.attached = true; + $("#right-panel").html(this.$el); + } + this.getContent(); + }, + + getContent: function() { + this.initPage(); + var _this = this; + + this.abuseReportsCollection.fetch({ + cache: false, + reset: true, + error: function(collection, response, opts) { + var err_msg = Common.prepareCollectionFetchErrorMsg(collection, response, opts); + _this.$error.html(err_msg).show(); + } + }) + }, + + addOne: function(report) { + var view = new AbuseReportView({model: report}); + this.$tableBody.append(view.render().el); + }, + + reset: function() { + this.initPage(); + + this.$loadingTip.hide(); + if (this.abuseReportsCollection.length > 0) { + this.abuseReportsCollection.each(this.addOne, this); + this.$table.show(); + } else { + this.$emptyTip.show(); + } + } + }); + return AbuseReportsView; +}); diff --git a/tests/api/endpoints/admin/test_abuse_reports.py b/tests/api/endpoints/admin/test_abuse_reports.py new file mode 100644 index 0000000000..d569c50a52 --- /dev/null +++ b/tests/api/endpoints/admin/test_abuse_reports.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +import json + +from mock import patch, MagicMock +from django.core.urlresolvers import reverse +from seahub.test_utils import BaseTestCase +from seahub.abuse_reports.models import AbuseReport + + +class AdminAbuseReportsTest(BaseTestCase): + + def setUp(self): + self.login_as(self.admin) + self.url = reverse('api-v2.1-admin-abuse-reports') + + @patch('seahub.api2.endpoints.admin.abuse_reports.ENABLE_SHARE_LINK_REPORT_ABUSE', MagicMock(return_value=True)) + def test_can_get(self): + resp = self.client.get(self.url) + self.assertEqual(200, resp.status_code) + + +class AdminAbuseReportTest(BaseTestCase): + def setUp(self): + self.login_as(self.admin) + self.repo = self.repo + self.file_path = self.file + self.url = reverse('api-v2.1-admin-abuse-reports') + + def _add_abuse_report(self): + reporter = '' + repo_id = self.repo.id + repo_name = self.repo.name + file_path = self.file_path + abuse_type = 'copyright' + description = '' + + report = AbuseReport.objects.add_abuse_report( + reporter, repo_id, repo_name, file_path, abuse_type, description) + return report + + def _remove_abuse_report(self, report_id): + report = AbuseReport.objects.get(id=report_id) + report.delete() + + @patch('seahub.api2.endpoints.admin.abuse_reports.ENABLE_SHARE_LINK_REPORT_ABUSE', MagicMock(return_value=True)) + def test_can_put(self): + report = self._add_abuse_report() + data = 'handled=' + str(not report.handled).lower() + resp = self.client.put(self.url + str(report.id) + '/', data, 'application/x-www-form-urlencoded') + self.assertEqual(200, resp.status_code) + + json_resp = json.loads(resp.content) + assert json_resp['file_name'] is not None + assert json_resp['time'] is not None + + assert json_resp['handled'] == (not report.handled) + assert json_resp['abuse_type'] == report.abuse_type + assert json_resp['description'] == report.description + assert json_resp['id'] == report.id + assert json_resp['reporter'] == report.reporter + assert json_resp['repo_id'] == report.repo_id + assert json_resp['repo_name'] == report.repo_name + assert json_resp['file_path'] == report.file_path + + self._remove_abuse_report(report.id) diff --git a/tests/api/endpoints/test_abuse_reports.py b/tests/api/endpoints/test_abuse_reports.py new file mode 100644 index 0000000000..0ff14c7815 --- /dev/null +++ b/tests/api/endpoints/test_abuse_reports.py @@ -0,0 +1,102 @@ +# -*- coding: utf-8 -*- +import json + +from mock import patch, MagicMock +from django.core.urlresolvers import reverse +from seahub.test_utils import BaseTestCase +from seahub.share.models import FileShare + + +class AbuseReportsTest(BaseTestCase): + + def setUp(self): + self.repo_id = self.repo.id + self.file_path = self.file + self.folder_path = self.folder + self.url = reverse('api-v2.1-abuse-reports') + self.inner_file_path = self.create_file( + repo_id=self.repo.id, + parent_dir='/folder/', + filename='inner.txt', + username='test@test.com') + + def tearDown(self): + self.remove_repo() + + def _add_file_share_link(self): + fs = FileShare.objects.create_file_link( + self.user.username, self.repo.id, self.file, None, None) + + return fs.token + + def _add_dir_share_link(self): + fs = FileShare.objects.create_dir_link( + self.user.username, self.repo.id, self.folder, None, None) + + return fs.token + + def _remove_share_link(self, token): + link = FileShare.objects.get(token=token) + link.delete() + + @patch('seahub.api2.endpoints.abuse_reports.ENABLE_SHARE_LINK_REPORT_ABUSE', MagicMock(return_value=True)) + def test_file_share_link_can_report(self): + self.login_as(self.user) + shared_token = self._add_file_share_link() + + data = { + 'share_link_token': shared_token, + 'abuse_type': 'copyright', + 'description': '', + 'reporter': '', + 'file_path': self.file_path, + } + + resp = self.client.post(self.url, data) + self.assertEqual(200, resp.status_code) + + json_resp = json.loads(resp.content) + assert json_resp['handled'] is not None + assert json_resp['id'] is not None + assert json_resp['file_name'] is not None + assert json_resp['repo_id'] is not None + assert json_resp['repo_name'] is not None + assert json_resp['time'] is not None + + assert data['file_path'] == json_resp['file_path'] + assert data['reporter'] == json_resp['reporter'] + assert data['description'] == json_resp['description'] + assert data['abuse_type'] == json_resp['abuse_type'] + + self._remove_share_link(shared_token) + + @patch('seahub.api2.endpoints.abuse_reports.ENABLE_SHARE_LINK_REPORT_ABUSE', MagicMock(return_value=True)) + def test_dir_share_link_can_report(self): + self.login_as(self.user) + shared_token = self._add_file_share_link() + + data = { + 'share_link_token': shared_token, + 'abuse_type': 'copyright', + 'description': '', + 'reporter': '', + 'file_path': self.inner_file_path, + } + + resp = self.client.post(self.url, data) + self.assertEqual(200, resp.status_code) + + json_resp = json.loads(resp.content) + assert json_resp['handled'] is not None + assert json_resp['id'] is not None + assert json_resp['file_name'] is not None + assert json_resp['repo_id'] is not None + assert json_resp['repo_name'] is not None + assert json_resp['time'] is not None + assert json_resp['file_path'] is not None + + assert data['reporter'] == json_resp['reporter'] + assert data['description'] == json_resp['description'] + assert data['abuse_type'] == json_resp['abuse_type'] + + self._remove_share_link(shared_token)