diff --git a/.travis.yml b/.travis.yml index bae95835e3..1c4e8acbc0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,7 +14,6 @@ install: - sudo apt-get purge -y mysql-common - wget https://raw.githubusercontent.com/frappe/bench/master/install_scripts/setup_frappe.sh - sudo bash setup_frappe.sh --skip-setup-bench --mysql-root-password travis - - sudo pip install --upgrade pip - rm $TRAVIS_BUILD_DIR/.git/shallow - cd ~/ && bench init frappe-bench --frappe-path $TRAVIS_BUILD_DIR - cp -r $TRAVIS_BUILD_DIR/test_sites/test_site ~/frappe-bench/sites/ diff --git a/frappe/app.py b/frappe/app.py index f1bbb28519..af3a489e01 100644 --- a/frappe/app.py +++ b/frappe/app.py @@ -72,6 +72,9 @@ def application(request): elif frappe.request.path.startswith('/backups'): response = frappe.utils.response.download_backup(request.path) + elif frappe.request.path.startswith('/private/files/'): + response = frappe.utils.response.download_private_file(request.path) + elif frappe.local.request.method in ('GET', 'HEAD'): response = frappe.website.render.render(request.path) diff --git a/frappe/change_log/current/private_files.md b/frappe/change_log/current/private_files.md new file mode 100644 index 0000000000..661e04f8b4 --- /dev/null +++ b/frappe/change_log/current/private_files.md @@ -0,0 +1,4 @@ +- Attachments can now be marked as **Private** + - Private files cannot be accessed unless you are logged in + - To access a private file, you need to have read permission on the file or read permission on the document to which the file is attached + - All attachments in a new incoming email are private diff --git a/frappe/commands.py b/frappe/commands.py index cf2ebec80a..784efdb64c 100644 --- a/frappe/commands.py +++ b/frappe/commands.py @@ -868,21 +868,23 @@ def use(site, sites_path='.'): @click.command('backup') @click.option('--with-files', default=False, is_flag=True, help="Take backup with files") @pass_context -def backup(context, with_files=False, backup_path_db=None, backup_path_files=None, quiet=False): +def backup(context, with_files=False, backup_path_db=None, backup_path_files=None, + backup_path_private_files=None, quiet=False): "Backup" from frappe.utils.backups import scheduled_backup verbose = context.verbose for site in context.sites: frappe.init(site=site) frappe.connect() - odb = scheduled_backup(ignore_files=not with_files, backup_path_db=backup_path_db, backup_path_files=backup_path_files, force=True) + odb = scheduled_backup(ignore_files=not with_files, backup_path_db=backup_path_db, backup_path_files=backup_path_files, backup_path_private_files=backup_path_private_files, force=True) if verbose: from frappe.utils import now print "database backup taken -", odb.backup_path_db, "- on", now() if with_files: print "files backup taken -", odb.backup_path_files, "- on", now() - frappe.destroy() + print "private files backup taken -", odb.backup_path_private_files, "- on", now() + frappe.destroy() @click.command('remove-from-installed-apps') @click.argument('app') diff --git a/frappe/core/doctype/file/file.js b/frappe/core/doctype/file/file.js index 7f05e2ff9a..bdcd6cf119 100644 --- a/frappe/core/doctype/file/file.js +++ b/frappe/core/doctype/file/file.js @@ -1,7 +1,11 @@ frappe.ui.form.on("File", "refresh", function(frm) { if(!frm.doc.is_folder) { frm.add_custom_button(__('Download'), function() { - window.open(frm.doc.file_url); + var file_url = frm.doc.file_url; + if (frm.doc.file_name) { + file_url = file_url.replace(/#/g, '%23'); + } + window.open(file_url); }, "icon-download"); } diff --git a/frappe/core/doctype/file/file.json b/frappe/core/doctype/file/file.json index f561fee71d..7c7b2a0a2a 100644 --- a/frappe/core/doctype/file/file.json +++ b/frappe/core/doctype/file/file.json @@ -25,6 +25,7 @@ "oldfieldtype": "Data", "permlevel": 0, "print_hide": 0, + "print_hide_if_no_value": 0, "read_only": 1, "report_hide": 0, "reqd": 0, @@ -32,6 +33,31 @@ "set_only_once": 0, "unique": 0 }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "depends_on": "eval:!doc.is_folder", + "fieldname": "is_private", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Is Private", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 1, + "unique": 0 + }, { "allow_on_submit": 0, "bold": 0, @@ -48,6 +74,7 @@ "permlevel": 0, "precision": "", "print_hide": 0, + "print_hide_if_no_value": 0, "read_only": 0, "report_hide": 0, "reqd": 0, @@ -71,6 +98,7 @@ "permlevel": 0, "precision": "", "print_hide": 0, + "print_hide_if_no_value": 0, "read_only": 0, "report_hide": 0, "reqd": 0, @@ -93,6 +121,7 @@ "permlevel": 0, "precision": "", "print_hide": 0, + "print_hide_if_no_value": 0, "read_only": 0, "report_hide": 0, "reqd": 0, @@ -117,6 +146,7 @@ "permlevel": 0, "precision": "", "print_hide": 0, + "print_hide_if_no_value": 0, "read_only": 0, "report_hide": 0, "reqd": 0, @@ -140,6 +170,7 @@ "permlevel": 0, "precision": "", "print_hide": 0, + "print_hide_if_no_value": 0, "read_only": 0, "report_hide": 0, "reqd": 0, @@ -162,6 +193,7 @@ "no_copy": 0, "permlevel": 0, "print_hide": 0, + "print_hide_if_no_value": 0, "read_only": 1, "report_hide": 0, "reqd": 0, @@ -184,6 +216,7 @@ "permlevel": 0, "precision": "", "print_hide": 0, + "print_hide_if_no_value": 0, "read_only": 0, "report_hide": 0, "reqd": 0, @@ -207,6 +240,7 @@ "no_copy": 0, "permlevel": 0, "print_hide": 0, + "print_hide_if_no_value": 0, "read_only": 1, "report_hide": 0, "reqd": 0, @@ -230,6 +264,7 @@ "permlevel": 0, "precision": "", "print_hide": 0, + "print_hide_if_no_value": 0, "read_only": 1, "report_hide": 0, "reqd": 0, @@ -254,6 +289,7 @@ "permlevel": 0, "precision": "", "print_hide": 0, + "print_hide_if_no_value": 0, "read_only": 1, "report_hide": 0, "reqd": 0, @@ -277,6 +313,7 @@ "permlevel": 0, "precision": "", "print_hide": 0, + "print_hide_if_no_value": 0, "read_only": 1, "report_hide": 0, "reqd": 0, @@ -300,6 +337,7 @@ "permlevel": 0, "precision": "", "print_hide": 0, + "print_hide_if_no_value": 0, "read_only": 0, "report_hide": 0, "reqd": 0, @@ -323,6 +361,7 @@ "options": "DocType", "permlevel": 0, "print_hide": 0, + "print_hide_if_no_value": 0, "read_only": 1, "report_hide": 0, "reqd": 0, @@ -345,6 +384,7 @@ "permlevel": 0, "precision": "", "print_hide": 0, + "print_hide_if_no_value": 0, "read_only": 0, "report_hide": 0, "reqd": 0, @@ -367,6 +407,7 @@ "no_copy": 0, "permlevel": 0, "print_hide": 0, + "print_hide_if_no_value": 0, "read_only": 1, "report_hide": 0, "reqd": 0, @@ -389,6 +430,7 @@ "no_copy": 0, "permlevel": 0, "print_hide": 0, + "print_hide_if_no_value": 0, "read_only": 0, "report_hide": 0, "reqd": 0, @@ -412,6 +454,7 @@ "permlevel": 0, "precision": "", "print_hide": 0, + "print_hide_if_no_value": 0, "read_only": 0, "report_hide": 0, "reqd": 0, @@ -435,6 +478,7 @@ "permlevel": 0, "precision": "", "print_hide": 0, + "print_hide_if_no_value": 0, "read_only": 0, "report_hide": 0, "reqd": 0, @@ -458,6 +502,7 @@ "permlevel": 0, "precision": "", "print_hide": 0, + "print_hide_if_no_value": 0, "read_only": 0, "report_hide": 0, "reqd": 0, @@ -476,7 +521,8 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2015-11-16 06:29:47.191098", + "menu_index": 0, + "modified": "2015-12-08 05:03:48.767257", "modified_by": "Administrator", "module": "Core", "name": "File", diff --git a/frappe/core/doctype/file/file.py b/frappe/core/doctype/file/file.py index 4391f76296..f32caa370b 100644 --- a/frappe/core/doctype/file/file.py +++ b/frappe/core/doctype/file/file.py @@ -108,6 +108,9 @@ class File(NestedSet): frappe.throw(_("Folder is mandatory")) def validate_file(self): + """Validates existence of public file + TODO: validate for private file + """ if (self.file_url or "").startswith("/files/"): if not self.file_name: self.file_name = self.file_url.split("/files/")[-1] @@ -337,3 +340,12 @@ def get_web_image(file_url): filename = "/files/" + strip(urllib.unquote(filename)) return image, filename, extn + +def check_file_permission(file_url): + for file in frappe.get_all("File", filters={"file_url": file_url, "is_private": 1}, fields=["name", "attached_to_doctype", "attached_to_name"]): + + if (frappe.has_permission("File", ptype="read", doc=file.name) + or frappe.has_permission(file.attached_to_doctype, ptype="read", doc=file.attached_to_name)): + return True + + raise frappe.PermissionError diff --git a/frappe/core/doctype/file/file_list.js b/frappe/core/doctype/file/file_list.js index 20b860fa7d..536a28b9a2 100644 --- a/frappe/core/doctype/file/file_list.js +++ b/frappe/core/doctype/file/file_list.js @@ -3,7 +3,7 @@ frappe.provide("frappe.ui"); frappe.listview_settings['File'] = { hide_name_column: true, use_route: true, - add_fields: ["is_folder", "file_name", "file_url", "folder"], + add_fields: ["is_folder", "file_name", "file_url", "folder", "is_private"], formatters: { file_size: function(value) { // formatter for file size @@ -17,13 +17,20 @@ frappe.listview_settings['File'] = { }, prepare_data: function(data) { // set image icons + var icon = "" + if(data.is_folder) { - data._title = ' ' + data.file_name; + icon += ' '; } else if(frappe.utils.is_image_file(data.file_name)) { - data._title = ' ' + data.file_name; + icon += ' '; } else { - data._title = ' \ - ' + (data.file_name ? data.file_name : data.file_url); + icon += ' ' + } + + data._title = icon + (data.file_name ? data.file_name : data.file_url) + + if (data.is_private) { + data._title += ' ' } }, onload: function(doclist) { @@ -87,7 +94,9 @@ frappe.listview_settings['File'] = { frappe.upload.upload_file(dataTransfer.files[0], { "folder": doclist.current_folder, "from_form": 1 - }, {}); + }, { + confirm_is_private: 1 + }); }); }, add_menu_item_copy: function(doclist){ diff --git a/frappe/desk/form/load.py b/frappe/desk/form/load.py index 3c82f0c07c..2dd1c2d1a5 100644 --- a/frappe/desk/form/load.py +++ b/frappe/desk/form/load.py @@ -100,7 +100,7 @@ def get_user_permissions(meta): return out def get_attachments(dt, dn): - return frappe.get_all("File", fields=["name", "file_name", "file_url"], + return frappe.get_all("File", fields=["name", "file_name", "file_url", "is_private"], filters = {"attached_to_name": dn, "attached_to_doctype": dt}) def get_comments(dt, dn, limit=100): @@ -121,11 +121,11 @@ def get_comments(dt, dn, limit=100): as_dict=True) for c in communications: - c.attachments = json.dumps([f.file_url for f in frappe.get_all("File", - fields=["file_url"], + c.attachments = json.dumps(frappe.get_all("File", + fields=["file_url", "is_private"], filters={"attached_to_doctype": "Communication", "attached_to_name": c.name} - )]) + )) return comments + communications diff --git a/frappe/desk/page/backups/backups.py b/frappe/desk/page/backups/backups.py index 9f798f6841..0cf31c5aa3 100644 --- a/frappe/desk/page/backups/backups.py +++ b/frappe/desk/page/backups/backups.py @@ -20,5 +20,6 @@ def get_context(context): files = [('/backups/' + _file, get_time(os.path.join(path, _file)), get_size(os.path.join(path, _file))) for _file in files] + files.sort(key=lambda x: x[1], reverse=True) return {"files": files} diff --git a/frappe/docs/current/api/utils/frappe.utils.backups.html b/frappe/docs/current/api/utils/frappe.utils.backups.html index 0663d9c92e..1b05924bcf 100644 --- a/frappe/docs/current/api/utils/frappe.utils.backups.html +++ b/frappe/docs/current/api/utils/frappe.utils.backups.html @@ -32,7 +32,7 @@ If specifying dbfilename, also append ".sql.gz"
__init__ - (self, db_name, user, password, backup_path_db=None, backup_path_files=None, db_host=localhost) + (self, db_name, user, password, backup_path_db=None, backup_path_files=None, backup_path_private_files=None, db_host=localhost)No docs
No docs
this function is called from scheduler deletes backups older than 7 days diff --git a/frappe/docs/current/api/utils/frappe.utils.file_manager.html b/frappe/docs/current/api/utils/frappe.utils.file_manager.html index f96efd57fc..5cce4577d6 100644 --- a/frappe/docs/current/api/utils/frappe.utils.file_manager.html +++ b/frappe/docs/current/api/utils/frappe.utils.file_manager.html @@ -164,7 +164,7 @@ frappe.utils.file_manager.get_file_data_from_hash - (content_hash) + (content_hash, is_private=0)
No docs
No docs
No docs
No docs
write file to disk with a random name (to compare)
+ + + frappe.utils.response.download_private_file + (path) +
+Checks permissions and sends back private file
+diff --git a/frappe/docs/current/index.html b/frappe/docs/current/index.html index f29a91858d..2d32403343 100644 --- a/frappe/docs/current/index.html +++ b/frappe/docs/current/index.html @@ -35,7 +35,7 @@ Version
6.12.3
+ 6.12.4
is_privatepreviewpreview_htmlsection_break_5is_home_folderis_attachments_folderfile_sizecolumn_break_5file_urlthumbnail_urlfolderis_foldersection_break_8attached_to_doctypecolumn_break_10attached_to_namecontent_hashlftrgtold_parentNo docs
+Validates existence of public file +TODO: validate for private file
+ + + frappe.core.doctype.file.file.check_file_permission + (file_url) +
+No docs
+Public API
/api/method/frappe.core.doctype.file.file.create_new_folder
+ + + frappe.integrations.doctype.dropbox_backup.dropbox_backup.upload_from_folder + (path, dropbox_folder, dropbox_client, did_not_upload, error_log) +
+No docs
+