Skip to content

Commit bb608ca

Browse files
committed
Merge branch 'v12-pre-release' into version-12
2 parents b856fa4 + 9c7680b commit bb608ca

File tree

31 files changed

+317
-361
lines changed

31 files changed

+317
-361
lines changed

.flake8

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
[flake8]
2+
ignore =
3+
E121,
4+
E126,
5+
E127,
6+
E128,
7+
E203,
8+
E225,
9+
E226,
10+
E231,
11+
E241,
12+
E251,
13+
E261,
14+
E265,
15+
E302,
16+
E303,
17+
E305,
18+
E402,
19+
E501,
20+
E741,
21+
W291,
22+
W292,
23+
W293,
24+
W391,
25+
W503,
26+
W504,
27+
F403,
28+
B007,
29+
B950,
30+
W191,
31+
32+
max-line-length = 200

cypress/integration/form.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,21 @@ context('Form', () => {
3939
list_view.filter_area.filter_list.clear_filters();
4040
});
4141
});
42+
it('validates behaviour of Data options validations in child table', () => {
43+
// test email validations for set_invalid controller
44+
let website_input = 'website.in';
45+
let expectBackgroundColor = 'rgb(255, 220, 220)';
46+
47+
cy.visit('/desk#Form/Contact/New Contact 1');
48+
cy.get('.frappe-control[data-fieldname="email_ids"]').as('table');
49+
cy.get('@table').find('button.grid-add-row').click();
50+
cy.get('.grid-body .rows [data-fieldname="email_id"]').click();
51+
cy.get('@table').find('input.input-with-feedback.form-control').as('email_input');
52+
cy.get('@email_input').type(website_input, { waitForAnimations: false });
53+
cy.fill_field('company_name', 'Test Company');
54+
cy.get('@email_input').should($div => {
55+
const style = window.getComputedStyle($div[0]);
56+
expect(style.backgroundColor).to.equal(expectBackgroundColor);
57+
});
58+
});
4259
});

frappe/__init__.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
reload(sys)
2424
sys.setdefaultencoding("utf-8")
2525

26-
__version__ = '12.16.3'
26+
__version__ = '12.17.0'
2727
__title__ = "Frappe Framework"
2828

2929
local = Local()
@@ -794,18 +794,19 @@ def get_meta_module(doctype):
794794
return frappe.modules.load_doctype_module(doctype)
795795

796796
def delete_doc(doctype=None, name=None, force=0, ignore_doctypes=None, for_reload=False,
797-
ignore_permissions=False, flags=None, ignore_on_trash=False, ignore_missing=True):
797+
ignore_permissions=False, flags=None, ignore_on_trash=False, ignore_missing=True, delete_permanently=False):
798798
"""Delete a document. Calls `frappe.model.delete_doc.delete_doc`.
799799
800800
:param doctype: DocType of document to be delete.
801801
:param name: Name of document to be delete.
802802
:param force: Allow even if document is linked. Warning: This may lead to data integrity errors.
803803
:param ignore_doctypes: Ignore if child table is one of these.
804804
:param for_reload: Call `before_reload` trigger before deleting.
805-
:param ignore_permissions: Ignore user permissions."""
805+
:param ignore_permissions: Ignore user permissions.
806+
:param delete_permanently: Do not create a Deleted Document for the document."""
806807
import frappe.model.delete_doc
807808
frappe.model.delete_doc.delete_doc(doctype, name, force, ignore_doctypes, for_reload,
808-
ignore_permissions, flags, ignore_on_trash, ignore_missing)
809+
ignore_permissions, flags, ignore_on_trash, ignore_missing, delete_permanently)
809810

810811
def delete_doc_if_exists(doctype, name, force=0):
811812
"""Delete document if exists."""

frappe/change_log/v12/v12_17_0.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
## Version 12.17.0 Release Notes
2+
3+
### Fixes & Enhancements
4+
5+
- Handle empty conditions in s3 backup ([#11258](https://github.com/frappe/frappe/pull/11258))
6+
- Auto delete Prepared Reports permanently ([#12683](https://github.com/frappe/frappe/pull/12683))
7+
- Always validate file URLs (backport) ([#12708](https://github.com/frappe/frappe/pull/12708))
8+
- Email validation improvements ([#12670](https://github.com/frappe/frappe/pull/12670))
9+
- Average Chart compute inaccurate average with test cases ([#12686](https://github.com/frappe/frappe/pull/12686))

frappe/core/doctype/file/file.py

Lines changed: 83 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -93,52 +93,89 @@ def validate(self):
9393
self.set_file_name()
9494
self.validate_duplicate_entry()
9595
self.validate_attachment_limit()
96+
9697
self.validate_folder()
9798

98-
if not self.file_url and not self.flags.ignore_file_validate:
99-
if not self.is_folder:
99+
if self.is_folder:
100+
self.file_url = ""
101+
else:
102+
self.validate_url()
103+
104+
self.file_size = frappe.form_dict.file_size or self.file_size
105+
106+
def validate_url(self):
107+
if not self.file_url or self.file_url.startswith(("http://", "https://")):
108+
if not self.flags.ignore_file_validate:
100109
self.validate_file()
101-
self.generate_content_hash()
102110

103-
self.validate_url()
111+
return
104112

105-
if frappe.db.exists('File', {'name': self.name, 'is_folder': 0}):
106-
old_file_url = self.file_url
107-
if not self.is_folder and (self.is_private != self.db_get('is_private')):
108-
private_files = frappe.get_site_path('private', 'files')
109-
public_files = frappe.get_site_path('public', 'files')
113+
# Probably an invalid web URL
114+
if not self.file_url.startswith(("/files/", "/private/files/")):
115+
frappe.throw(
116+
_("URL must start with http:// or https://"),
117+
title=_('Invalid URL')
118+
)
119+
120+
# Ensure correct formatting and type
121+
self.file_url = unquote(self.file_url)
122+
self.is_private = cint(self.is_private)
123+
124+
self.handle_is_private_changed()
125+
126+
base_path = os.path.realpath(get_files_path(is_private=self.is_private))
127+
if not os.path.realpath(self.get_full_path()).startswith(base_path):
128+
frappe.throw(
129+
_("The File URL you've entered is incorrect"),
130+
title=_('Invalid File URL')
131+
)
132+
133+
def handle_is_private_changed(self):
134+
if not frappe.db.exists(
135+
'File', {
136+
'name': self.name,
137+
'is_private': cint(not self.is_private)
138+
}
139+
):
140+
return
110141

111-
file_name = self.file_url.split('/')[-1]
112-
if not self.is_private:
113-
shutil.move(os.path.join(private_files, file_name),
114-
os.path.join(public_files, file_name))
142+
old_file_url = self.file_url
115143

116-
self.file_url = "/files/{0}".format(file_name)
144+
file_name = self.file_url.split('/')[-1]
145+
private_file_path = frappe.get_site_path('private', 'files', file_name)
146+
public_file_path = frappe.get_site_path('public', 'files', file_name)
117147

118-
else:
119-
shutil.move(os.path.join(public_files, file_name),
120-
os.path.join(private_files, file_name))
148+
if self.is_private:
149+
shutil.move(public_file_path, private_file_path)
150+
url_starts_with = "/private/files/"
151+
else:
152+
shutil.move(private_file_path, public_file_path)
153+
url_starts_with = "/files/"
121154

122-
self.file_url = "/private/files/{0}".format(file_name)
155+
self.file_url = "{0}{1}".format(url_starts_with, file_name)
156+
update_existing_file_docs(self)
123157

124-
update_existing_file_docs(self)
158+
if (
159+
not self.attached_to_doctype
160+
or not self.attached_to_name
161+
or not self.fetch_attached_to_field(old_file_url)
162+
):
163+
return
125164

126-
# update documents image url with new file url
127-
if self.attached_to_doctype and self.attached_to_name:
128-
if not self.attached_to_field:
129-
field_name = None
130-
reference_dict = frappe.get_doc(self.attached_to_doctype, self.attached_to_name).as_dict()
131-
for key, value in reference_dict.items():
132-
if value == old_file_url:
133-
field_name = key
134-
break
135-
self.attached_to_field = field_name
136-
if self.attached_to_field:
137-
frappe.db.set_value(self.attached_to_doctype, self.attached_to_name,
138-
self.attached_to_field, self.file_url)
139-
140-
if self.file_url and (self.is_private != self.file_url.startswith('/private')):
141-
frappe.throw(_('Invalid file URL. Please contact System Administrator.'))
165+
frappe.db.set_value(self.attached_to_doctype, self.attached_to_name,
166+
self.attached_to_field, self.file_url)
167+
168+
def fetch_attached_to_field(self, old_file_url):
169+
if self.attached_to_field:
170+
return True
171+
172+
reference_dict = frappe.get_doc(
173+
self.attached_to_doctype, self.attached_to_name).as_dict()
174+
175+
for key, value in reference_dict.items():
176+
if value == old_file_url:
177+
self.attached_to_field = key
178+
return True
142179

143180
def validate_attachment_limit(self):
144181
attachment_limit = 0
@@ -331,8 +368,13 @@ def exists_on_disk(self):
331368

332369
def get_content(self):
333370
"""Returns [`file_name`, `content`] for given file name `fname`"""
371+
if self.is_folder:
372+
frappe.throw(_("Cannot get file contents of a Folder"))
373+
334374
if self.get('content'):
335375
return self.content
376+
377+
self.validate_url()
336378
file_path = self.get_full_path()
337379

338380
# read the file
@@ -419,17 +461,6 @@ def save_uploaded(self):
419461
else:
420462
raise Exception
421463

422-
423-
def validate_url(self, df=None):
424-
if self.file_url:
425-
if not self.file_url.startswith(("http://", "https://", "/files/", "/private/files/")):
426-
frappe.throw(_("URL must start with 'http://' or 'https://'"))
427-
return
428-
429-
self.file_url = unquote(self.file_url)
430-
self.file_size = frappe.form_dict.file_size or self.file_size
431-
432-
433464
def get_uploaded_content(self):
434465
# should not be unicode when reading a file, hence using frappe.form
435466
if 'filedata' in frappe.form_dict:
@@ -692,7 +723,7 @@ def delete_file(path):
692723
os.remove(path)
693724

694725

695-
def remove_file(fid=None, attached_to_doctype=None, attached_to_name=None, from_delete=False):
726+
def remove_file(fid=None, attached_to_doctype=None, attached_to_name=None, from_delete=False, delete_permanently=False):
696727
"""Remove file and File entry"""
697728
file_name = None
698729
if not (attached_to_doctype and attached_to_name):
@@ -710,7 +741,7 @@ def remove_file(fid=None, attached_to_doctype=None, attached_to_name=None, from_
710741
if not file_name:
711742
file_name = frappe.db.get_value("File", fid, "file_name")
712743
comment = doc.add_comment("Attachment Removed", _("Removed {0}").format(file_name))
713-
frappe.delete_doc("File", fid, ignore_permissions=ignore_permissions)
744+
frappe.delete_doc("File", fid, ignore_permissions=ignore_permissions, delete_permanently=delete_permanently)
714745

715746
return comment
716747

@@ -719,17 +750,18 @@ def get_max_file_size():
719750
return conf.get('max_file_size') or 10485760
720751

721752

722-
def remove_all(dt, dn, from_delete=False):
753+
def remove_all(dt, dn, from_delete=False, delete_permanently=False):
723754
"""remove all files in a transaction"""
724755
try:
725756
for fid in frappe.db.sql_list("""select name from `tabFile` where
726757
attached_to_doctype=%s and attached_to_name=%s""", (dt, dn)):
727758
if from_delete:
728759
# If deleting a doc, directly delete files
729-
frappe.delete_doc("File", fid, ignore_permissions=True)
760+
frappe.delete_doc("File", fid, ignore_permissions=True, delete_permanently=delete_permanently)
730761
else:
731762
# Removes file and adds a comment in the document it is attached to
732-
remove_file(fid=fid, attached_to_doctype=dt, attached_to_name=dn, from_delete=from_delete)
763+
remove_file(fid=fid, attached_to_doctype=dt, attached_to_name=dn,
764+
from_delete=from_delete, delete_permanently=delete_permanently)
733765
except Exception as e:
734766
if e.args[0]!=1054: raise # (temp till for patched)
735767

frappe/core/doctype/file/test_file.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -192,8 +192,6 @@ def tearDown(self):
192192

193193

194194
class TestFile(unittest.TestCase):
195-
196-
197195
def setUp(self):
198196
self.delete_test_data()
199197
self.upload_file()
@@ -375,3 +373,20 @@ def test_website_user_file_permission(self):
375373
# because user gets permission via has_web_form_permission
376374
self.assertIsInstance(txt_file, File)
377375
frappe.set_user('Administrator')
376+
377+
def test_parent_directory_validation_in_file_url(self):
378+
file1 = frappe.get_doc({
379+
"doctype": "File",
380+
"file_name": 'parent_dir.txt',
381+
"attached_to_doctype": "",
382+
"attached_to_name": "",
383+
"is_private": 1,
384+
"content": test_content1}).insert()
385+
386+
file1.file_url = '/private/files/../test.txt'
387+
self.assertRaises(frappe.exceptions.ValidationError, file1.save)
388+
389+
# No validation to see if file exists
390+
file1.reload()
391+
file1.file_url = '/private/files/parent_dir2.txt'
392+
file1.save()

frappe/core/doctype/prepared_report/prepared_report.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,6 @@ def before_insert(self):
2424
def enqueue_report(self):
2525
enqueue(run_background, prepared_report=self.name, timeout=6000)
2626

27-
def on_trash(self):
28-
remove_all("Prepared Report", self.name)
2927

3028

3129
def run_background(prepared_report):
@@ -100,7 +98,7 @@ def delete_expired_prepared_reports():
10098
def delete_prepared_reports(reports):
10199
reports = frappe.parse_json(reports)
102100
for report in reports:
103-
frappe.delete_doc('Prepared Report', report['name'], ignore_permissions=True)
101+
frappe.delete_doc('Prepared Report', report['name'], ignore_permissions=True, delete_permanently=True)
104102

105103
def create_json_gz_file(data, dt, dn):
106104
# Storing data in CSV file causes information loss

frappe/core/doctype/system_settings/system_settings.json

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,7 @@
5959
"enable_prepared_report_auto_deletion",
6060
"prepared_report_expiry_period",
6161
"chat",
62-
"enable_chat",
63-
"use_socketio_to_upload_file"
62+
"enable_chat"
6463
],
6564
"fields": [
6665
{
@@ -384,12 +383,6 @@
384383
"fieldtype": "Check",
385384
"label": "Enable Chat"
386385
},
387-
{
388-
"default": "1",
389-
"fieldname": "use_socketio_to_upload_file",
390-
"fieldtype": "Check",
391-
"label": "Use socketio to upload file"
392-
},
393386
{
394387
"fieldname": "column_break_21",
395388
"fieldtype": "Column Break"
@@ -404,7 +397,7 @@
404397
{
405398
"default": "7",
406399
"depends_on": "enable_prepared_report_auto_deletion",
407-
"description": "System will automatically delete Prepared Reports after these many days since creation",
400+
"description": "System will auto-delete Prepared Reports permanently after these many days since creation",
408401
"fieldname": "prepared_report_expiry_period",
409402
"fieldtype": "Int",
410403
"label": "Prepared Report Expiry Period (Days)"
@@ -424,7 +417,8 @@
424417
],
425418
"icon": "fa fa-cog",
426419
"issingle": 1,
427-
"modified": "2020-07-26 15:57:34.197718",
420+
"links": [],
421+
"modified": "2021-03-25 17:54:32.668876",
428422
"modified_by": "Administrator",
429423
"module": "Core",
430424
"name": "System Settings",

0 commit comments

Comments
 (0)