Merge branch 'develop' into fix-list-view-header
This commit is contained in:
commit
fbdb00a68d
37 changed files with 7931 additions and 7373 deletions
|
|
@ -227,17 +227,17 @@ context("Control Link", () => {
|
|||
field_name: "assigned_by",
|
||||
property: "default",
|
||||
property_type: "Text",
|
||||
value: "Administrator",
|
||||
value: cy.config("testUser"),
|
||||
},
|
||||
true
|
||||
);
|
||||
cy.reload();
|
||||
cy.new_form("ToDo");
|
||||
cy.fill_field("description", "new", "Text Editor").wait(200);
|
||||
cy.fill_field("description", "new", "Text Editor").blur().wait(200);
|
||||
cy.save();
|
||||
cy.get(".frappe-control[data-fieldname=assigned_by_full_name] .control-value").should(
|
||||
"contain",
|
||||
"Administrator"
|
||||
"Frappe"
|
||||
);
|
||||
// if user clears default value explicitly, system should not reset default again
|
||||
cy.get_field("assigned_by").clear().blur();
|
||||
|
|
|
|||
|
|
@ -924,7 +924,7 @@ def get_installed_apps(*, _ensure_on_bench: bool = False) -> list[str]:
|
|||
|
||||
def get_doc_hooks():
|
||||
"""Return hooked methods for given doc. Expand the dict tuple if required."""
|
||||
if not hasattr(local, "doc_events_hooks"):
|
||||
if not getattr(local, "doc_events_hooks", None):
|
||||
hooks = get_hooks("doc_events", {})
|
||||
out = {}
|
||||
for key, value in hooks.items():
|
||||
|
|
|
|||
|
|
@ -437,14 +437,6 @@ def validate_link(doctype: str, docname: str, fields=None):
|
|||
if not values.name:
|
||||
return values
|
||||
|
||||
if not frappe.has_permission(doctype, "read", doc=values.name):
|
||||
frappe.throw(
|
||||
_("You do not have permission to access {0} {1}").format(
|
||||
frappe.bold(doctype), frappe.bold(docname)
|
||||
),
|
||||
frappe.PermissionError,
|
||||
)
|
||||
|
||||
if not fields:
|
||||
frappe.local.response_headers.set("Cache-Control", "private,max-age=1800,stale-while-revalidate=7200")
|
||||
return values
|
||||
|
|
|
|||
|
|
@ -57,9 +57,8 @@ class Address(Document):
|
|||
self.flags.linked = False
|
||||
|
||||
def autoname(self):
|
||||
if not self.address_title:
|
||||
if self.links:
|
||||
self.address_title = self.links[0].link_name
|
||||
if not self.address_title and self.links:
|
||||
self.address_title = self.links[0].link_title or self.links[0].link_name
|
||||
|
||||
if self.address_title:
|
||||
self.name = cstr(self.address_title).strip() + "-" + cstr(_(self.address_type)).strip()
|
||||
|
|
|
|||
|
|
@ -1199,6 +1199,7 @@ class Database:
|
|||
"""`ROLLBACK` current transaction. Optionally rollback to a known save_point."""
|
||||
if save_point:
|
||||
self.sql(f"rollback to savepoint {save_point}")
|
||||
self.value_cache.clear()
|
||||
elif not self._disable_transaction_control:
|
||||
self.before_commit.reset()
|
||||
self.after_commit.reset()
|
||||
|
|
|
|||
|
|
@ -359,6 +359,7 @@ def remove_from_installed_apps(app_name):
|
|||
"DefaultValue", {"defkey": "installed_apps"}, "defvalue", json.dumps(installed_apps)
|
||||
)
|
||||
_clear_cache("__global")
|
||||
frappe.local.doc_events_hooks = None
|
||||
frappe.get_single("Installed Applications").update_versions()
|
||||
frappe.db.commit()
|
||||
if frappe.flags.in_install:
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -8,7 +8,11 @@ import json
|
|||
import re
|
||||
from collections import Counter
|
||||
from collections.abc import Mapping, Sequence
|
||||
from functools import cached_property
|
||||
from functools import cached_property, lru_cache
|
||||
|
||||
import sqlparse
|
||||
from sqlparse import tokens
|
||||
from sqlparse.sql import Function, Parenthesis, Statement
|
||||
|
||||
import frappe
|
||||
import frappe.defaults
|
||||
|
|
@ -33,6 +37,22 @@ from frappe.utils import (
|
|||
)
|
||||
from frappe.utils.data import DateTimeLikeObject, get_datetime, getdate, sbool
|
||||
|
||||
|
||||
@lru_cache(maxsize=128)
|
||||
def _parse_sql(field: str) -> Statement | None:
|
||||
"""
|
||||
Parse a given SQL statement using `sqlparse`.
|
||||
|
||||
Args:
|
||||
field (str): The SQL statement string to parse.
|
||||
|
||||
Returns:
|
||||
Statement | None: A `sqlparse.sql.Statement` object if parsing succeeds, otherwise `None`.
|
||||
"""
|
||||
if parsed := sqlparse.parse(field):
|
||||
return parsed[0]
|
||||
|
||||
|
||||
LOCATE_PATTERN = re.compile(r"locate\([^,]+,\s*[`\"]?name[`\"]?\s*\)", flags=re.IGNORECASE)
|
||||
LOCATE_CAST_PATTERN = re.compile(r"locate\(([^,]+),\s*([`\"]?name[`\"]?)\s*\)", flags=re.IGNORECASE)
|
||||
FUNC_IFNULL_PATTERN = re.compile(r"(strpos|ifnull|coalesce)\(\s*[`\"]?name[`\"]?\s*,", flags=re.IGNORECASE)
|
||||
|
|
@ -456,6 +476,46 @@ from {tables}
|
|||
"sleep",
|
||||
]
|
||||
|
||||
def _find_subqueries(parsed: Statement) -> list:
|
||||
"""
|
||||
Recursively find all subqueries in a parsed SQL statement.
|
||||
"""
|
||||
subqueries = []
|
||||
|
||||
for token in parsed.tokens:
|
||||
if isinstance(token, Parenthesis):
|
||||
# Check for DML token for subquery check
|
||||
is_subquery = False
|
||||
for sub_token in token.tokens:
|
||||
if sub_token.ttype is tokens.DML:
|
||||
is_subquery = True
|
||||
break
|
||||
if is_subquery:
|
||||
subqueries.append(token)
|
||||
# Recursively check for nested subqueries
|
||||
subqueries.extend(_find_subqueries(token))
|
||||
elif token.is_group:
|
||||
subqueries.extend(_find_subqueries(token))
|
||||
|
||||
return subqueries
|
||||
|
||||
def _check_sql_token(statement: Statement) -> None:
|
||||
"""
|
||||
Checks the output of `sqlparse.parse()` to detect blocked functions and subqueries.
|
||||
"""
|
||||
if _find_subqueries(statement):
|
||||
_raise_exception()
|
||||
|
||||
for token in statement.tokens:
|
||||
if isinstance(token, Function):
|
||||
if (name := (token.get_name())) and name.lower() in blacklisted_functions:
|
||||
_raise_exception()
|
||||
if token.ttype == tokens.Keyword:
|
||||
if token.value.lower() in blacklisted_keywords:
|
||||
_raise_exception()
|
||||
if token.is_group:
|
||||
_check_sql_token(token)
|
||||
|
||||
def _raise_exception():
|
||||
frappe.throw(_("Use of sub-query or function is restricted"), frappe.DataError)
|
||||
|
||||
|
|
@ -470,21 +530,8 @@ from {tables}
|
|||
lower_field = field.lower().strip()
|
||||
|
||||
if SUB_QUERY_PATTERN.match(field):
|
||||
# Check for subquery anywhere in the field, not just at the beginning
|
||||
if "(" in lower_field:
|
||||
# Check all parentheses pairs, not just the first one
|
||||
paren_start = 0
|
||||
while True:
|
||||
location = lower_field.find("(", paren_start)
|
||||
if location == -1:
|
||||
break
|
||||
token = lower_field[location + 1 :].lstrip().split(" ", 1)[0]
|
||||
if any(
|
||||
re.search(r"\b" + re.escape(keyword) + r"\b", token)
|
||||
for keyword in blacklisted_keywords + blacklisted_functions
|
||||
):
|
||||
_raise_exception()
|
||||
paren_start = location + 1
|
||||
# Check all tokens for subquery detection
|
||||
_check_sql_token(_parse_sql(field))
|
||||
|
||||
if "@" in lower_field:
|
||||
# prevent access to global variables
|
||||
|
|
|
|||
|
|
@ -1585,7 +1585,10 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
|
|||
fields: this.get_dialog_fields(),
|
||||
primary_action: (values) => {
|
||||
// doctype fields
|
||||
let fields = values[this.doctype].map((f) => [f, this.doctype]);
|
||||
let fields = (values[this.doctype] || []).map((f) => [
|
||||
f,
|
||||
this.doctype,
|
||||
]);
|
||||
delete values[this.doctype];
|
||||
|
||||
// child table fields
|
||||
|
|
@ -1610,6 +1613,18 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
|
|||
},
|
||||
});
|
||||
|
||||
const $bulk = $(`
|
||||
<div class="mb-3">
|
||||
<button class="btn btn-default btn-xs" data-action="select_all">${__("Select All")}</button>
|
||||
<button class="btn btn-default btn-xs" data-action="unselect_all">${__("Unselect All")}</button>
|
||||
</div>
|
||||
`);
|
||||
const toggleAll = (checked) =>
|
||||
d.$wrapper.find(":checkbox").prop("checked", checked).trigger("change");
|
||||
$bulk.on("click", "[data-action=select_all]", () => toggleAll(true));
|
||||
$bulk.on("click", "[data-action=unselect_all]", () => toggleAll(false));
|
||||
d.$body.prepend($bulk);
|
||||
|
||||
d.$body.prepend(`
|
||||
<div class="columns-search">
|
||||
<input type="text" placeholder="${__(
|
||||
|
|
|
|||
|
|
@ -489,6 +489,23 @@ class TestDBQuery(IntegrationTestCase):
|
|||
)
|
||||
self.assertTrue("_relevance" in data[0])
|
||||
|
||||
# Test that fields with keywords in strings are allowed
|
||||
data = DatabaseQuery("DocType").execute(
|
||||
fields=["name", "locate('select', name)"],
|
||||
limit_start=0,
|
||||
limit_page_length=1,
|
||||
)
|
||||
self.assertTrue(data)
|
||||
|
||||
# Test that subqueries with other DML are blocked
|
||||
self.assertRaises(
|
||||
frappe.DataError,
|
||||
DatabaseQuery("DocType").execute,
|
||||
fields=["name", "issingle", "(insert into tabUser values (1))"],
|
||||
limit_start=0,
|
||||
limit_page_length=1,
|
||||
)
|
||||
|
||||
data = DatabaseQuery("DocType").execute(
|
||||
fields=["name", "issingle", "date(creation) as creation"],
|
||||
limit_start=0,
|
||||
|
|
@ -554,6 +571,16 @@ class TestDBQuery(IntegrationTestCase):
|
|||
limit_page_length=1,
|
||||
)
|
||||
|
||||
# Ensure search terms aren't blocked as functions
|
||||
from frappe.desk.search import search_link
|
||||
|
||||
search_terms = ("global", "user")
|
||||
|
||||
for term in search_terms:
|
||||
with self.subTest(term=term):
|
||||
result = search_link("ToDo", term)
|
||||
self.assertIsInstance(result, list)
|
||||
|
||||
def test_nested_permission(self):
|
||||
frappe.set_user("Administrator")
|
||||
create_nested_doctype()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue