1# Copyright (c) 2012-2016 Seafile Ltd. 2# -*- coding: utf-8 -*- 3import os 4import stat 5import logging 6import json 7import posixpath 8import csv 9import chardet 10import io 11 12from django.urls import reverse 13from django.http import HttpResponse, Http404 14from django.template.loader import render_to_string 15from django.utils.http import urlquote 16from django.utils.html import escape 17from django.utils.timezone import now 18from django.utils.translation import ugettext as _ 19from django.conf import settings as dj_settings 20from django.template.defaultfilters import filesizeformat 21 22import seaserv 23from seaserv import seafile_api, ccnet_api, \ 24 seafserv_threaded_rpc 25from pysearpc import SearpcError 26 27from seahub.auth.decorators import login_required_ajax 28from seahub.base.decorators import require_POST 29from seahub.forms import RepoRenameDirentForm 30from seahub.options.models import UserOptions, CryptoOptionNotSetError 31from seahub.notifications.models import UserNotification 32from seahub.notifications.views import add_notice_from_info 33from seahub.share.models import UploadLinkShare 34from seahub.signals import upload_file_successful 35from seahub.views import get_unencry_rw_repos_by_user, \ 36 get_diff, check_folder_permission 37from seahub.group.utils import is_group_member, is_group_admin_or_owner, \ 38 get_group_member_info 39import seahub.settings as settings 40from seahub.settings import ENABLE_THUMBNAIL, THUMBNAIL_ROOT, \ 41 THUMBNAIL_DEFAULT_SIZE, SHOW_TRAFFIC, MEDIA_URL, ENABLE_VIDEO_THUMBNAIL 42from seahub.utils import check_filename_with_rename, EMPTY_SHA1, \ 43 gen_block_get_url, \ 44 new_merge_with_no_conflict, get_commit_before_new_merge, \ 45 gen_file_upload_url, is_org_context, is_pro_version, normalize_dir_path, \ 46 FILEEXT_TYPE_MAP 47from seahub.utils.star import get_dir_starred_files 48from seahub.utils.file_types import IMAGE, VIDEO, XMIND 49from seahub.utils.file_op import check_file_lock, ONLINE_OFFICE_LOCK_OWNER 50from seahub.utils.repo import get_locked_files_by_dir, get_repo_owner, \ 51 repo_has_been_shared_out, parse_repo_perm 52from seahub.utils.error_msg import file_type_error_msg, file_size_error_msg 53from seahub.base.accounts import User 54from seahub.thumbnail.utils import get_thumbnail_src 55from seahub.share.utils import is_repo_admin 56from seahub.base.templatetags.seahub_tags import translate_seahub_time, \ 57 email2nickname, tsstr_sec 58from seahub.constants import PERMISSION_READ_WRITE 59from seahub.constants import HASH_URLS 60 61# Get an instance of a logger 62logger = logging.getLogger(__name__) 63 64########## Seafile API Wrapper 65def get_repo(repo_id): 66 return seafile_api.get_repo(repo_id) 67 68def get_commit(repo_id, repo_version, commit_id): 69 return seaserv.get_commit(repo_id, repo_version, commit_id) 70 71def get_group(gid): 72 return seaserv.get_group(gid) 73 74########## repo related 75def convert_repo_path_when_can_not_view_folder(request, repo_id, path): 76 77 content_type = 'application/json; charset=utf-8' 78 79 path = normalize_dir_path(path) 80 username = request.user.username 81 converted_repo_path = seafile_api.convert_repo_path(repo_id, path, username) 82 if not converted_repo_path: 83 err_msg = _('Permission denied.') 84 return HttpResponse(json.dumps({'error': err_msg}), 85 status=403, content_type=content_type) 86 87 converted_repo_path = json.loads(converted_repo_path) 88 89 repo_id = converted_repo_path['repo_id'] 90 repo = seafile_api.get_repo(repo_id) 91 if not repo: 92 err_msg = 'Library not found.' 93 return HttpResponse(json.dumps({'error': err_msg}), 94 status=404, content_type=content_type) 95 96 path = converted_repo_path['path'] 97 path = normalize_dir_path(path) 98 dir_id = seafile_api.get_dir_id_by_path(repo.id, path) 99 if not dir_id: 100 err_msg = 'Folder not found.' 101 return HttpResponse(json.dumps({'error': err_msg}), 102 status=404, content_type=content_type) 103 104 group_id = '' 105 if 'group_id' in converted_repo_path: 106 group_id = converted_repo_path['group_id'] 107 if not ccnet_api.get_group(group_id): 108 err_msg = 'Group not found.' 109 return HttpResponse(json.dumps({'error': err_msg}), 110 status=404, content_type=content_type) 111 112 if not is_group_member(group_id, username): 113 err_msg = _('Permission denied.') 114 return HttpResponse(json.dumps({'error': err_msg}), 115 status=403, content_type=content_type) 116 117 user_perm = check_folder_permission(request, repo_id, path) 118 if not user_perm: 119 err_msg = _('Permission denied.') 120 return HttpResponse(json.dumps({'error': err_msg}), 121 status=403, content_type=content_type) 122 123 if not group_id: 124 next_url = '#shared-libs/lib/%s/%s' % (repo_id, path.strip('/')) 125 else: 126 next_url = '#group/%s/lib/%s/%s' % (group_id, repo_id, path.strip('/')) 127 128 return HttpResponse(json.dumps({'next_url': next_url}), content_type=content_type) 129 130@login_required_ajax 131def list_lib_dir(request, repo_id): 132 ''' 133 New ajax API for list library directory 134 ''' 135 content_type = 'application/json; charset=utf-8' 136 result = {} 137 138 repo = get_repo(repo_id) 139 if not repo: 140 err_msg = _('Library does not exist.') 141 return HttpResponse(json.dumps({'error': err_msg}), 142 status=400, content_type=content_type) 143 144 username = request.user.username 145 146 path = request.GET.get('p', '/') 147 path = normalize_dir_path(path) 148 dir_id = seafile_api.get_dir_id_by_path(repo.id, path) 149 if not dir_id: 150 err_msg = 'Folder not found.' 151 return HttpResponse(json.dumps({'error': err_msg}), 152 status=404, content_type=content_type) 153 154 # perm for current dir 155 user_perm = check_folder_permission(request, repo_id, path) 156 if not user_perm: 157 return convert_repo_path_when_can_not_view_folder(request, repo_id, path) 158 159 if repo.encrypted \ 160 and not seafile_api.is_password_set(repo.id, username): 161 err_msg = _('Library is encrypted.') 162 return HttpResponse(json.dumps({'error': err_msg, 'lib_need_decrypt': True}), 163 status=403, content_type=content_type) 164 165 head_commit = get_commit(repo.id, repo.version, repo.head_cmmt_id) 166 if not head_commit: 167 err_msg = _('Error: no head commit id') 168 return HttpResponse(json.dumps({'error': err_msg}), 169 status=500, content_type=content_type) 170 171 dir_list = [] 172 file_list = [] 173 174 175 dirs = seafserv_threaded_rpc.list_dir_with_perm(repo_id, path, dir_id, 176 username, -1, -1) 177 starred_files = get_dir_starred_files(username, repo_id, path) 178 179 for dirent in dirs: 180 dirent.last_modified = dirent.mtime 181 if stat.S_ISDIR(dirent.mode): 182 dpath = posixpath.join(path, dirent.obj_name) 183 if dpath[-1] != '/': 184 dpath += '/' 185 dir_list.append(dirent) 186 else: 187 if repo.version == 0: 188 file_size = seafile_api.get_file_size(repo.store_id, repo.version, dirent.obj_id) 189 else: 190 file_size = dirent.size 191 dirent.file_size = file_size if file_size else 0 192 193 dirent.starred = False 194 fpath = posixpath.join(path, dirent.obj_name) 195 if fpath in starred_files: 196 dirent.starred = True 197 198 file_list.append(dirent) 199 200 if is_org_context(request): 201 repo_owner = seafile_api.get_org_repo_owner(repo.id) 202 else: 203 repo_owner = seafile_api.get_repo_owner(repo.id) 204 205 result["repo_owner"] = repo_owner 206 result["is_repo_owner"] = False 207 result["has_been_shared_out"] = False 208 result["is_admin"] = is_repo_admin(username, repo_id) 209 if repo_owner == username: 210 result["is_repo_owner"] = True 211 try: 212 result["has_been_shared_out"] = repo_has_been_shared_out(request, repo_id) 213 except Exception as e: 214 logger.error(e) 215 216 if result["is_admin"]: 217 result["has_been_shared_out"] = True 218 219 result["is_virtual"] = repo.is_virtual 220 result["repo_name"] = repo.name 221 result["user_perm"] = user_perm 222 # check quota for fileupload 223 result["no_quota"] = True if seaserv.check_quota(repo.id) < 0 else False 224 result["encrypted"] = repo.encrypted 225 226 dirent_list = [] 227 for d in dir_list: 228 d_ = {} 229 d_['is_dir'] = True 230 d_['obj_name'] = d.obj_name 231 d_['last_modified'] = d.last_modified 232 d_['last_update'] = translate_seahub_time(d.last_modified) 233 d_['p_dpath'] = posixpath.join(path, d.obj_name) 234 d_['perm'] = d.permission # perm for sub dir in current dir 235 dirent_list.append(d_) 236 237 size = int(request.GET.get('thumbnail_size', THUMBNAIL_DEFAULT_SIZE)) 238 239 for f in file_list: 240 f_ = {} 241 f_['is_file'] = True 242 f_['obj_name'] = f.obj_name 243 f_['last_modified'] = f.last_modified 244 f_['last_update'] = translate_seahub_time(f.last_modified) 245 f_['starred'] = f.starred 246 f_['file_size'] = filesizeformat(f.file_size) 247 f_['obj_id'] = f.obj_id 248 f_['perm'] = f.permission # perm for file in current dir 249 250 if not repo.encrypted and ENABLE_THUMBNAIL: 251 # used for providing a way to determine 252 # if send a request to create thumbnail. 253 254 fileExt = os.path.splitext(f.obj_name)[1][1:].lower() 255 file_type = FILEEXT_TYPE_MAP.get(fileExt) 256 if file_type == IMAGE: 257 f_['is_img'] = True 258 259 if file_type == VIDEO and ENABLE_VIDEO_THUMBNAIL: 260 f_['is_video'] = True 261 262 if file_type == XMIND: 263 f_['is_xmind'] = True 264 265 if file_type in (IMAGE, XMIND) or \ 266 file_type == VIDEO and ENABLE_VIDEO_THUMBNAIL: 267 # if thumbnail has already been created, return its src. 268 # Then web browser will use this src to get thumbnail instead of 269 # recreating it. 270 thumbnail_file_path = os.path.join(THUMBNAIL_ROOT, str(size), f.obj_id) 271 thumbnail_exist = os.path.exists(thumbnail_file_path) 272 if thumbnail_exist: 273 file_path = posixpath.join(path, f.obj_name) 274 src = get_thumbnail_src(repo_id, size, file_path) 275 f_['encoded_thumbnail_src'] = urlquote(src) 276 277 if is_pro_version(): 278 f_['is_locked'] = True if f.is_locked else False 279 f_['lock_owner'] = f.lock_owner 280 f_['lock_owner_name'] = email2nickname(f.lock_owner) 281 282 f_['locked_by_me'] = False 283 if f.lock_owner == username: 284 f_['locked_by_me'] = True 285 286 if f.lock_owner == ONLINE_OFFICE_LOCK_OWNER and \ 287 user_perm == PERMISSION_READ_WRITE: 288 f_['locked_by_me'] = True 289 290 dirent_list.append(f_) 291 292 result["dirent_list"] = dirent_list 293 294 return HttpResponse(json.dumps(result), content_type=content_type) 295 296 297def upload_file_done(request): 298 """Send a message when a file is uploaded. 299 300 Arguments: 301 - `request`: 302 """ 303 ct = 'application/json; charset=utf-8' 304 result = {} 305 306 filename = request.GET.get('fn', '') 307 if not filename: 308 result['error'] = _('Argument missing') 309 return HttpResponse(json.dumps(result), status=400, content_type=ct) 310 repo_id = request.GET.get('repo_id', '') 311 if not repo_id: 312 result['error'] = _('Argument missing') 313 return HttpResponse(json.dumps(result), status=400, content_type=ct) 314 path = request.GET.get('p', '') 315 if not path: 316 result['error'] = _('Argument missing') 317 return HttpResponse(json.dumps(result), status=400, content_type=ct) 318 319 # a few checkings 320 if not seafile_api.get_repo(repo_id): 321 result['error'] = _('Wrong repo id') 322 return HttpResponse(json.dumps(result), status=400, content_type=ct) 323 324 # get upload link share creator 325 token = request.GET.get('token', '') 326 if not token: 327 result['error'] = _('Argument missing') 328 return HttpResponse(json.dumps(result), status=400, content_type=ct) 329 330 uls = UploadLinkShare.objects.get_valid_upload_link_by_token(token) 331 if uls is None: 332 result['error'] = _('Bad upload link token.') 333 return HttpResponse(json.dumps(result), status=400, content_type=ct) 334 creator = uls.username 335 336 file_path = path.rstrip('/') + '/' + filename 337 if seafile_api.get_file_id_by_path(repo_id, file_path) is None: 338 result['error'] = _('File does not exist') 339 return HttpResponse(json.dumps(result), status=400, content_type=ct) 340 341 # send singal 342 upload_file_successful.send(sender=None, 343 repo_id=repo_id, 344 file_path=file_path, 345 owner=creator) 346 347 return HttpResponse(json.dumps({'success': True}), content_type=ct) 348 349 350def get_file_upload_url_ul(request, token): 351 """Get file upload url in dir upload link. 352 353 Arguments: 354 - `request`: 355 - `token`: 356 """ 357 if not request.is_ajax(): 358 raise Http404 359 360 content_type = 'application/json; charset=utf-8' 361 362 uls = UploadLinkShare.objects.get_valid_upload_link_by_token(token) 363 if uls is None: 364 return HttpResponse(json.dumps({"error": _("Bad upload link token.")}), 365 status=400, content_type=content_type) 366 367 shared_by = uls.username 368 repo_id = uls.repo_id 369 r = request.GET.get('r', '') 370 if repo_id != r: # perm check 371 return HttpResponse(json.dumps({"error": _("Bad repo id in upload link.")}), 372 status=403, content_type=content_type) 373 374 repo = get_repo(repo_id) 375 if not repo: 376 return HttpResponse(json.dumps({"error": _("Library does not exist")}), 377 status=404, content_type=content_type) 378 379 if repo.encrypted or \ 380 seafile_api.check_permission_by_path(repo_id, '/', shared_by) != 'rw': 381 return HttpResponse(json.dumps({"error": _("Permission denied")}), 382 status=403, content_type=content_type) 383 384 dir_id = seafile_api.get_dir_id_by_path(uls.repo_id, uls.path) 385 if not dir_id: 386 return HttpResponse(json.dumps({"error": _("Directory does not exist.")}), 387 status=404, content_type=content_type) 388 389 obj_id = json.dumps({'parent_dir': uls.path}) 390 args = [repo_id, obj_id, 'upload-link', shared_by] 391 kwargs = { 392 'use_onetime': False, 393 } 394 if (is_pro_version() and dj_settings.ENABLE_UPLOAD_LINK_VIRUS_CHECK): 395 kwargs.update({'check_virus': True}) 396 397 try: 398 acc_token = seafile_api.get_fileserver_access_token(*args, **kwargs) 399 except SearpcError as e: 400 logger.error(e) 401 return HttpResponse(json.dumps({"error": _("Internal Server Error")}), 402 status=500, content_type=content_type) 403 404 if not acc_token: 405 return HttpResponse(json.dumps({"error": _("Internal Server Error")}), 406 status=500, content_type=content_type) 407 408 url = gen_file_upload_url(acc_token, 'upload-aj') 409 return HttpResponse(json.dumps({"url": url}), content_type=content_type) 410 411@login_required_ajax 412def repo_history_changes(request, repo_id): 413 changes = {} 414 content_type = 'application/json; charset=utf-8' 415 416 repo = get_repo(repo_id) 417 if not repo: 418 err_msg = _('Library does not exist.') 419 return HttpResponse(json.dumps({'error': err_msg}), 420 status=400, content_type=content_type) 421 422 # perm check 423 if check_folder_permission(request, repo_id, '/') is None: 424 if request.user.is_staff is True: 425 pass # Allow system staff to check repo changes 426 else: 427 err_msg = _("Permission denied") 428 return HttpResponse(json.dumps({"error": err_msg}), status=403, 429 content_type=content_type) 430 431 username = request.user.username 432 try: 433 server_crypto = UserOptions.objects.is_server_crypto(username) 434 except CryptoOptionNotSetError: 435 # Assume server_crypto is ``False`` if this option is not set. 436 server_crypto = False 437 438 if repo.encrypted and \ 439 (repo.enc_version == 1 or (repo.enc_version == 2 and server_crypto)) \ 440 and not seafile_api.is_password_set(repo_id, username): 441 err_msg = _('Library is encrypted.') 442 return HttpResponse(json.dumps({'error': err_msg}), 443 status=403, content_type=content_type) 444 445 commit_id = request.GET.get('commit_id', '') 446 if not commit_id: 447 err_msg = _('Argument missing') 448 return HttpResponse(json.dumps({'error': err_msg}), 449 status=400, content_type=content_type) 450 451 changes = get_diff(repo_id, '', commit_id) 452 453 c = get_commit(repo.id, repo.version, commit_id) 454 if c.parent_id is None: 455 # A commit is a first commit only if it's parent id is None. 456 changes['cmt_desc'] = repo.desc 457 elif c.second_parent_id is None: 458 # Normal commit only has one parent. 459 if c.desc.startswith('Changed library'): 460 changes['cmt_desc'] = _('Changed library name or description') 461 else: 462 # A commit is a merge only if it has two parents. 463 changes['cmt_desc'] = _('No conflict in the merge.') 464 465 changes['date_time'] = tsstr_sec(c.ctime) 466 467 return HttpResponse(json.dumps(changes), content_type=content_type) 468