diff --git a/media/css/seahub.css b/media/css/seahub.css
index a1a0396372..f1c2142298 100644
--- a/media/css/seahub.css
+++ b/media/css/seahub.css
@@ -889,6 +889,11 @@ textarea:-moz-placeholder {/* for FF */
font-size:14px;
margin:5px 0 20px;
}
+.new-narrow-panel .notice {
+ text-align: center;
+ margin-top:20px;
+ color:#666;
+}
/**** wide panel ****/ /* e.g. repo decrypt page */
.wide-panel {
width: 928px;
@@ -1011,6 +1016,10 @@ textarea:-moz-placeholder {/* for FF */
.op-confirm button {
margin-right:8px;
}
+/**** forms ****/
+.field-feedback {
+ color:#666;
+}
/**** simplemodal ****/
#basic-modal-content {
display:none;
@@ -3446,3 +3455,10 @@ img.thumbnail {
.device-libs-dropdown-menu {
min-width:200px;
}
+/* link audit */
+#link-audit-form .email-input {
+ margin-bottom:5px;
+}
+#link-audit-form .get-code-btn {
+ margin-bottom:20px;
+}
diff --git a/seahub/settings.py b/seahub/settings.py
index a92b287753..1a92f69ec8 100644
--- a/seahub/settings.py
+++ b/seahub/settings.py
@@ -253,6 +253,9 @@ REPO_PASSWORD_MIN_LENGTH = 8
# mininum length for the password of a share link
SHARE_LINK_PASSWORD_MIN_LENGTH = 8
+# enable or disable share link audit
+ENABLE_SHARE_LINK_AUDIT = False
+
# mininum length for user's password
USER_PASSWORD_MIN_LENGTH = 6
diff --git a/seahub/share/decorators.py b/seahub/share/decorators.py
new file mode 100644
index 0000000000..c373aae73f
--- /dev/null
+++ b/seahub/share/decorators.py
@@ -0,0 +1,58 @@
+from django.core.cache import cache
+from django.conf import settings
+from django.http import Http404
+from django.shortcuts import render_to_response
+from django.template import RequestContext
+
+from seahub.share.models import FileShare, UploadLinkShare
+from seahub.utils import normalize_cache_key, is_pro_version
+
+def share_link_audit(func):
+ def _decorated(request, token, *args, **kwargs):
+ assert token is not None # Checked by URLconf
+
+ fileshare = FileShare.objects.get_valid_file_link_by_token(token)
+ if fileshare is None:
+ fileshare = UploadLinkShare.objects.get_valid_upload_link_by_token(token)
+ if fileshare is None:
+ raise Http404
+
+ if not is_pro_version() or not settings.ENABLE_SHARE_LINK_AUDIT:
+ return func(request, fileshare, *args, **kwargs)
+
+ # no audit for authenticated user, since we've already got email address
+ if request.user.is_authenticated():
+ return func(request, fileshare, *args, **kwargs)
+
+ # anonymous user
+ if request.session.get('anonymous_email') is not None:
+ request.user.username = request.session.get('anonymous_email')
+ return func(request, fileshare, *args, **kwargs)
+
+ if request.method == 'GET':
+ return render_to_response('share/share_link_audit.html', {
+ 'token': token,
+ }, context_instance=RequestContext(request))
+ elif request.method == 'POST':
+ code = request.POST.get('code', '')
+ email = request.POST.get('email', '')
+
+ cache_key = normalize_cache_key(email, 'share_link_audit_')
+ if code == cache.get(cache_key):
+ # code is correct, add this email to session so that he will
+ # not be asked again during this session, and clear this code.
+ request.session['anonymous_email'] = email
+ request.user.username = request.session.get('anonymous_email')
+ cache.delete(cache_key)
+ return func(request, fileshare, *args, **kwargs)
+ else:
+ return render_to_response('share/share_link_audit.html', {
+ 'err_msg': 'Invalid token, please try again.',
+ 'email': email,
+ 'code': code,
+ 'token': token,
+ }, context_instance=RequestContext(request))
+ else:
+ assert False, 'TODO'
+
+ return _decorated
diff --git a/seahub/share/models.py b/seahub/share/models.py
index 6befd5088d..3259491536 100644
--- a/seahub/share/models.py
+++ b/seahub/share/models.py
@@ -7,7 +7,8 @@ from django.utils.translation import ugettext_lazy as _
from django.contrib.auth.hashers import make_password, check_password
from seahub.base.fields import LowerCaseCharField
-from seahub.utils import normalize_file_path, normalize_dir_path, gen_token
+from seahub.utils import normalize_file_path, normalize_dir_path, gen_token,\
+ get_service_url
# Get an instance of a logger
logger = logging.getLogger(__name__)
@@ -179,6 +180,13 @@ class FileShare(models.Model):
def is_owner(self, owner):
return owner == self.username
+ def get_full_url(self):
+ service_url = get_service_url().rstrip('/')
+ if self.is_file_share_link():
+ return '%s/f/%s/' % (service_url, self.token)
+ else:
+ return '%s/d/%s/' % (service_url, self.token)
+
class OrgFileShareManager(models.Manager):
def set_org_file_share(self, org_id, file_share):
"""Set a share link as org share link.
diff --git a/seahub/share/templates/share/audit_code_email.html b/seahub/share/templates/share/audit_code_email.html
new file mode 100644
index 0000000000..beb6d691bd
--- /dev/null
+++ b/seahub/share/templates/share/audit_code_email.html
@@ -0,0 +1,16 @@
+{% extends 'email_base.html' %}
+{% load i18n %}
+
+{% block email_con %}
+
+{% autoescape off %}
+
+
{% trans "Hi," %}
+
+
+{% blocktrans %}Your code is {{code}}, this code will be valid for one hour.{% endblocktrans%}
+
+
+{% endautoescape %}
+
+{% endblock %}
diff --git a/seahub/share/templates/share/share_link_audit.html b/seahub/share/templates/share/share_link_audit.html
new file mode 100644
index 0000000000..55519ad888
--- /dev/null
+++ b/seahub/share/templates/share/share_link_audit.html
@@ -0,0 +1,85 @@
+{% extends "base.html" %}
+{% load i18n %}
+
+{% block main_panel %}
+
+
{% trans "Email Verification" %}
+
+
+{% endblock %}
+{% block extra_script %}
+
+{% endblock %}
diff --git a/seahub/share/urls.py b/seahub/share/urls.py
index 5837f097a8..ecb1f36fb2 100644
--- a/seahub/share/urls.py
+++ b/seahub/share/urls.py
@@ -18,4 +18,5 @@ urlpatterns = patterns('',
url(r'^ajax/get-download-link/$', ajax_get_download_link, name='ajax_get_download_link'),
url(r'^ajax/get-upload-link/$', ajax_get_upload_link, name='ajax_get_upload_link'),
url(r'^ajax/private-share-dir/$', ajax_private_share_dir, name='ajax_private_share_dir'),
+ url(r'^ajax/get-link-audit-code/$', ajax_get_link_audit_code, name='ajax_get_link_audit_code'),
)
diff --git a/seahub/share/views.py b/seahub/share/views.py
index 04c5124864..a966d406f6 100644
--- a/seahub/share/views.py
+++ b/seahub/share/views.py
@@ -5,6 +5,7 @@ import json
from dateutil.relativedelta import relativedelta
from constance import config
+from django.core.cache import cache
from django.core.urlresolvers import reverse
from django.http import HttpResponse, HttpResponseRedirect, Http404, \
HttpResponseBadRequest
@@ -33,7 +34,8 @@ from seahub.utils import render_permission_error, string2list, render_error, \
gen_shared_link, gen_shared_upload_link, gen_dir_share_link, \
gen_file_share_link, IS_EMAIL_CONFIGURED, check_filename_with_rename, \
is_valid_username, is_valid_email, send_html_email, is_org_context, \
- send_perm_audit_msg, get_origin_repo_info
+ send_perm_audit_msg, get_origin_repo_info, gen_token, normalize_cache_key
+from seahub.utils.mail import send_html_email_with_dj_template, MAIL_PRIORITY
from seahub.settings import SITE_ROOT, REPLACE_FROM_EMAIL, ADD_REPLY_TO_HEADER
from seahub.profile.models import Profile
@@ -1187,3 +1189,53 @@ def ajax_private_share_dir(request):
# for case: only share to users and the emails are not valid
data = json.dumps({"error": _("Please check the email(s) you entered")})
return HttpResponse(data, status=400, content_type=content_type)
+
+def ajax_get_link_audit_code(request):
+ """
+ Generate a token, and record that token with email in cache, expires in
+ one hour, send token to that email address.
+
+ User provide token and email at share link page, if the token and email
+ are valid, record that email in session.
+ """
+ content_type = 'application/json; charset=utf-8'
+
+ token = request.POST.get('token')
+ email = request.POST.get('email')
+ if not is_valid_email(email):
+ return HttpResponse(json.dumps({
+ 'error': _('Email address is not valid')
+ }), status=400, content_type=content_type)
+
+ dfs = FileShare.objects.get_valid_file_link_by_token(token)
+ ufs = UploadLinkShare.objects.get_valid_upload_link_by_token(token)
+
+ fs = dfs if dfs else ufs
+ if fs is None:
+ return HttpResponse(json.dumps({
+ 'error': _('Share link is not found')
+ }), status=400, content_type=content_type)
+
+ cache_key = normalize_cache_key(email, 'share_link_audit_')
+ timeout = 60 * 60 # one hour
+ code = gen_token(max_length=6)
+ cache.set(cache_key, code, timeout)
+
+ # send code to user via email
+ subject = _("Verification code for visiting share links")
+ c = {
+ 'code': code,
+ }
+ try:
+ send_html_email_with_dj_template(
+ email, dj_template='share/audit_code_email.html',
+ context=c, subject=subject, priority=MAIL_PRIORITY.now
+ )
+ return HttpResponse(json.dumps({'success': True}), status=200,
+ content_type=content_type)
+ except Exception as e:
+ logger.error('Failed to send audit code via email to %s')
+ logger.error(e)
+ return HttpResponse(json.dumps({
+ "error": _("Failed to send a verification code, please try again later.")
+ }), status=500, content_type=content_type)
diff --git a/seahub/views/file.py b/seahub/views/file.py
index b737712128..59fb28ff47 100644
--- a/seahub/views/file.py
+++ b/seahub/views/file.py
@@ -41,6 +41,7 @@ from seahub.avatar.templatetags.group_avatar_tags import grp_avatar
from seahub.auth.decorators import login_required
from seahub.base.decorators import repo_passwd_set_required
from seahub.share.models import FileShare, check_share_link_common
+from seahub.share.decorators import share_link_audit
from seahub.wiki.utils import get_wiki_dirent
from seahub.wiki.models import WikiDoesNotExist, WikiPageMissing
from seahub.utils import render_error, is_org_context, \
@@ -734,17 +735,14 @@ def _download_file_from_share_link(request, fileshare):
use_onetime=False)
return HttpResponseRedirect(gen_file_get_url(dl_token, filename))
-def view_shared_file(request, token):
+@share_link_audit
+def view_shared_file(request, fileshare):
"""
View file via shared link.
Download share file if `dl` in request param.
View raw share file if `raw` in request param.
"""
- assert token is not None # Checked by URLconf
-
- fileshare = FileShare.objects.get_valid_file_link_by_token(token)
- if fileshare is None:
- raise Http404
+ token = fileshare.token
password_check_passed, err_msg = check_share_link_common(request, fileshare)
if not password_check_passed:
@@ -1224,6 +1222,7 @@ def send_file_access_msg(request, repo, path, access_from):
- `access_from`: web or api
"""
username = request.user.username
+
ip = get_remote_ip(request)
user_agent = request.META.get("HTTP_USER_AGENT")
diff --git a/seahub/views/repo.py b/seahub/views/repo.py
index ae825780ce..f69b4d795f 100644
--- a/seahub/views/repo.py
+++ b/seahub/views/repo.py
@@ -16,6 +16,7 @@ from seaserv import seafile_api
from seahub.auth.decorators import login_required
from seahub.options.models import UserOptions, CryptoOptionNotSetError
+from seahub.share.decorators import share_link_audit
from seahub.share.models import FileShare, UploadLinkShare, \
check_share_link_common
from seahub.views import gen_path_link, get_repo_dirents, \
@@ -198,12 +199,9 @@ def _download_dir_from_share_link(request, fileshare, repo, real_path):
return HttpResponseRedirect(gen_file_get_url(token, dirname))
-def view_shared_dir(request, token):
- assert token is not None # Checked by URLconf
-
- fileshare = FileShare.objects.get_valid_dir_link_by_token(token)
- if fileshare is None:
- raise Http404
+@share_link_audit
+def view_shared_dir(request, fileshare):
+ token = fileshare.token
password_check_passed, err_msg = check_share_link_common(request, fileshare)
if not password_check_passed:
@@ -295,12 +293,9 @@ def view_shared_dir(request, token):
'thumbnail_size': thumbnail_size,
}, context_instance=RequestContext(request))
-def view_shared_upload_link(request, token):
- assert token is not None # Checked by URLconf
-
- uploadlink = UploadLinkShare.objects.get_valid_upload_link_by_token(token)
- if uploadlink is None:
- raise Http404
+@share_link_audit
+def view_shared_upload_link(request, uploadlink):
+ token = uploadlink.token
password_check_passed, err_msg = check_share_link_common(request,
uploadlink,