Merge branch 'master' into develop

This commit is contained in:
Nabin Hait 2017-03-09 14:59:02 +05:30
commit 7fe50c885a
21 changed files with 229 additions and 83 deletions

View file

@ -13,7 +13,7 @@ import os, sys, importlib, inspect, json
from .exceptions import *
from .utils.jinja import get_jenv, get_template, render_template
__version__ = '7.2.28'
__version__ = '7.2.29'
__title__ = "Frappe Framework"
local = Local()

View file

@ -235,8 +235,17 @@ class User(Document):
link = self.reset_password()
self.send_login_mail(_("Verify Your Account"), "templates/emails/new_user.html",
{"link": link, "site_url": get_url()})
app_title = [t for t in frappe.get_hooks('app_title') if t != 'Frappe Framework']
if app_title:
subject = _("Welcome to {0}").format(app_title[0])
else:
subject = _("Complete Registration")
self.send_login_mail(subject, "templates/emails/new_user.html",
dict(
link=link,
site_url=get_url(),
))
def send_login_mail(self, subject, template, add_args, now=None):

View file

@ -112,7 +112,7 @@ def get_std_fields_list(meta, key):
def build_for_autosuggest(res):
results = []
for r in res:
out = {"value": r[0], "description": ", ".join(unique(cstr(d) for d in r)[1:])}
out = {"value": r[0], "description": ", ".join(unique(cstr(d) for d in r if d)[1:])}
results.append(out)
return results

View file

@ -281,7 +281,7 @@ class DatabaseQuery(object):
can_be_null = True
# prepare in condition
if f.operator in ('in', 'not in'):
if f.operator.lower() in ('in', 'not in'):
values = f.value
if not isinstance(values, (list, tuple)):
values = values.split(",")
@ -296,7 +296,7 @@ class DatabaseQuery(object):
if df and df.fieldtype in ("Check", "Float", "Int", "Currency", "Percent"):
can_be_null = False
if f.operator=='Between' and \
if f.operator.lower() == 'between' and \
(f.fieldname in ('creation', 'modified') or (df and (df.fieldtype=="Date" or df.fieldtype=="Datetime"))):
value = "'%s' AND '%s'" % (
get_datetime(f.value[0]).strftime("%Y-%m-%d %H:%M:%S.%f"),
@ -315,12 +315,12 @@ class DatabaseQuery(object):
value = get_time(f.value).strftime("%H:%M:%S.%f")
fallback = "'00:00:00'"
elif f.operator in ("like", "not like") or (isinstance(f.value, basestring) and
elif f.operator.lower() in ("like", "not like") or (isinstance(f.value, basestring) and
(not df or df.fieldtype not in ["Float", "Int", "Currency", "Percent", "Check"])):
value = "" if f.value==None else f.value
fallback = '""'
if f.operator in ("like", "not like") and isinstance(value, basestring):
if f.operator.lower() in ("like", "not like") and isinstance(value, basestring):
# because "like" uses backslash (\) for escaping
value = value.replace("\\", "\\\\").replace("%", "%%")
@ -329,12 +329,12 @@ class DatabaseQuery(object):
fallback = 0
# put it inside double quotes
if isinstance(value, basestring) and not f.operator=='Between':
if isinstance(value, basestring) and not f.operator.lower() == 'between':
value = '"{0}"'.format(frappe.db.escape(value, percent=False))
if (self.ignore_ifnull
or not can_be_null
or (f.value and f.operator in ('=', 'like'))
or (f.value and f.operator.lower() in ('=', 'like'))
or 'ifnull(' in column_name.lower()):
condition = '{column_name} {operator} {value}'.format(
column_name=column_name, operator=f.operator,

View file

@ -112,9 +112,6 @@ a.badge-hover:focus .badge,
a.badge-hover:active .badge {
background-color: #D8DFE5;
}
.msgprint {
margin: 15px 0px;
}
.msgprint pre {
text-align: left;
}

View file

@ -112,9 +112,6 @@ a.badge-hover:focus .badge,
a.badge-hover:active .badge {
background-color: #D8DFE5;
}
.msgprint {
margin: 15px 0px;
}
.msgprint pre {
text-align: left;
}
@ -503,6 +500,7 @@ fieldset[disabled] .form-control {
color: #fff;
border-radius: 10px;
cursor: pointer;
margin-right: 10px;
}
/* on small screens, show only icons on top */
@media (max-width: 767px) {

View file

@ -3,6 +3,9 @@
border: 1px solid #d1d8dd;
border-radius: 3px;
}
.form-grid.error {
border-color: #ff5858;
}
.grid-heading-row {
border-bottom: 1px solid #d1d8dd;
background-color: #F7FAFC;
@ -55,7 +58,13 @@
padding: 10px 15px;
max-height: 200px;
border-right: 1px solid #d1d8dd;
margin-bottom: -1px;
}
.grid-static-col.bold {
font-weight: bold;
background-color: #fffdf4;
}
.validated-form .grid-static-col.error {
background-color: #FFDCDC;
}
.grid-static-col input[type="checkbox"] {
margin-left: -16px !important;

View file

@ -112,9 +112,6 @@ a.badge-hover:focus .badge,
a.badge-hover:active .badge {
background-color: #D8DFE5;
}
.msgprint {
margin: 15px 0px;
}
.msgprint pre {
text-align: left;
}

View file

@ -401,10 +401,10 @@ frappe.ui.form.ControlInput = frappe.ui.form.Control.extend({
},
set_bold: function() {
if(this.$input) {
this.$input.toggleClass("bold", !!this.df.bold);
this.$input.toggleClass("bold", !!(this.df.bold || this.df.reqd));
}
if(this.disp_area) {
$(this.disp_area).toggleClass("bold", !!this.df.bold);
$(this.disp_area).toggleClass("bold", !!(this.df.bold || this.df.reqd));
}
}
});

View file

@ -53,6 +53,8 @@ frappe.ui.form.Grid = Class.extend({
.appendTo(this.parent)
.attr("data-fieldname", this.df.fieldname);
this.form_grid = this.wrapper.find('.form-grid');
this.wrapper.find(".grid-add-row").click(function() {
me.add_new_row(null, null, true);
me.set_focus_on_row();
@ -206,6 +208,9 @@ frappe.ui.form.Grid = Class.extend({
frappe.utils.scroll_to(_scroll_y);
}
// red if mandatory
this.form_grid.toggleClass('error', !!(this.df.reqd && !(data && data.length)));
this.refresh_remove_rows_button();
},
setup_toolbar: function() {
@ -821,6 +826,15 @@ frappe.ui.form.GridRow = Class.extend({
this.refresh_field(df.fieldname, txt);
}
// background color for cellz
if(this.doc) {
if(df.reqd && !txt) {
column.addClass('error');
}
if (df.reqd || df.bold) {
column.addClass('bold');
}
}
}
},
@ -1085,8 +1099,9 @@ frappe.ui.form.GridRow = Class.extend({
}
},
refresh_field: function(fieldname, txt) {
var df = this.grid.get_docfield(fieldname);
if(txt===undefined) {
var txt = frappe.format(this.doc[fieldname], this.grid.get_docfield(fieldname),
var txt = frappe.format(this.doc[fieldname], df,
null, this.frm.doc);
}
@ -1094,6 +1109,9 @@ frappe.ui.form.GridRow = Class.extend({
var column = this.columns[fieldname];
if(column) {
column.static_area.html(txt || "");
if(df.reqd) {
column.toggleClass('error', !!(txt===null || txt===''));
}
}
// reset field value

View file

@ -19,6 +19,7 @@ frappe.ui.form.save = function(frm, action, callback, btn) {
var save = function() {
check_name(function() {
$(frm.wrapper).addClass('validated-form');
if(check_mandatory()) {
_call({
method: "frappe.desk.form.save.savedocs",
@ -132,10 +133,21 @@ frappe.ui.form.save = function(frm, action, callback, btn) {
}
});
if(error_fields.length)
msgprint(__('Mandatory fields required in {0}', [(doc.parenttype
? (__(frappe.meta.docfield_map[doc.parenttype][doc.parentfield].label) + ' ('+ __("Table") + ')')
: __(doc.doctype))]) + '<br> <ul><li>' + error_fields.join('</li><li>') + "</ul>");
if(error_fields.length) {
if(doc.parenttype) {
var message = __('Mandatory fields required in table {0}, Row {1}',
[__(frappe.meta.docfield_map[doc.parenttype][doc.parentfield].label).bold(), doc.idx]);
} else {
var message = __('Mandatory fields required in {0}', [__(doc.doctype)]);
}
message = message + '<br><br><ul><li>' + error_fields.join('</li><li>') + "</ul>";
msgprint({
message: message,
indicator: 'red',
title: __('Missing Fields')
});
}
});
return !has_errors;

View file

@ -13,8 +13,9 @@
<span class="open-notification hidden"
title="{{ __("Open {0}", [__(doctype)])}}"></span>
{% if !internal_links[doctype] %}
<button class="btn btn-new btn-default btn-xs pull-right hidden"
data-doctype="{{ doctype }}">{{ __("New") }}</button>
<button class="btn btn-new btn-default btn-xs hidden"
data-doctype="{{ doctype }}">
<i class="octicon octicon-plus" style="font-size: 12px;"></i></button>
{% endif %}
</div>
{% } %}

View file

@ -457,6 +457,7 @@ _f.Frm.prototype.refresh = function(docname) {
}
if(is_a_different_doc) {
$(this.wrapper).removeClass('validated-form')
if(this.show_print_first && this.doc.docstatus===1) {
// show print view
this.print_doc();

View file

@ -135,7 +135,7 @@ a.badge-hover& {
}
.msgprint {
margin: 15px 0px;
// margin: 15px 0px;
// text-align: center;
pre {

View file

@ -324,6 +324,7 @@ textarea.form-control {
color:#fff;
border-radius:10px;
cursor: pointer;
margin-right: 10px;
}
/* on small screens, show only icons on top */

View file

@ -6,6 +6,10 @@
border-radius: 3px;
}
.form-grid.error {
border-color: @indicator-red;
}
.grid-heading-row {
border-bottom: 1px solid @border-color;
background-color: @panel-bg;
@ -69,7 +73,15 @@
padding: 10px 15px;
max-height: 200px;
border-right: 1px solid @border-color;
margin-bottom: -1px;
}
.grid-static-col.bold {
font-weight: bold;
background-color: @extra-light-yellow;
}
.validated-form .grid-static-col.error {
background-color: @label-danger-bg;
}
.grid-static-col {

View file

@ -1,12 +1,17 @@
<h3>{{ title }}</h3>
{% if title %}<h3>{{ title }}</h3>{% endif %}
<p>{{_("Dear")}} {{ first_name }}{% if last_name %} {{ last_name}}{% endif %},</p>
<p>{{_("A new account has been created for you at {0}").format(site_url)}}.</p>
<p>{{_("Your login id is")}}: <b>{{ user }}</b>
<p>{{_("Click on the link below to complete your registration and set a new password")}}.</p>
<p><b><a href="{{ link }}">{{_("Complete Registration")}}</a></b></p>
<p style="margin: 30px 0px;">
<a href="{{ link }}" rel="nofollow" style="padding: 8px 20px; background-color: #7575ff; color: #fff; border-radius: 4px; text-decoration: none; line-height: 1; border-bottom: 3px solid rgba(0, 0, 0, 0.2); font-size: 14px; font-weight: 200;">{{ _("Complete Registration") }}</a>
</p>
{% if user_fullname != "Administrator" %}
<br>
<p>{{_("Thank you")}},<br>
{{ user_fullname }}</p>
{% endif %}
<br>
<p style="font-size: 85%;">{{_("You can also copy-paste this link in your browser")}} <a href="{{ link }}">{{ link }}</a></p>

View file

@ -6,9 +6,8 @@
from __future__ import unicode_literals
from werkzeug.test import Client
import os, re, urllib, sys, json, md5, requests, traceback
import bleach, bleach_whitelist
from html5lib.sanitizer import HTMLSanitizer
from markdown2 import markdown as _markdown
from .html_utils import sanitize_html
import frappe
from frappe.utils.identicon import Identicon
@ -441,51 +440,6 @@ def watch(path, handler=None, debug=True):
observer.stop()
observer.join()
def sanitize_html(html, linkify=False):
"""
Sanitize HTML tags, attributes and style to prevent XSS attacks
Based on bleach clean, bleach whitelist and HTML5lib's Sanitizer defaults
Does not sanitize JSON, as it could lead to future problems
"""
if not isinstance(html, basestring):
return html
elif is_json(html):
return html
tags = (HTMLSanitizer.acceptable_elements + HTMLSanitizer.svg_elements
+ ["html", "head", "meta", "link", "body", "iframe", "style", "o:p"])
attributes = {"*": HTMLSanitizer.acceptable_attributes, "svg": HTMLSanitizer.svg_attributes}
styles = bleach_whitelist.all_styles
strip_comments = False
# retuns html with escaped tags, escaped orphan >, <, etc.
escaped_html = bleach.clean(html, tags=tags, attributes=attributes, styles=styles, strip_comments=strip_comments)
if linkify:
# based on bleach.clean
class s(bleach.BleachSanitizer):
allowed_elements = tags
allowed_attributes = attributes
allowed_css_properties = styles
strip_disallowed_elements = False
strip_html_comments = strip_comments
escaped_html = bleach.linkify(escaped_html, tokenizer=s)
return escaped_html
def is_json(text):
try:
json.loads(text)
except ValueError:
return False
else:
return True
def markdown(text, sanitize=True, linkify=True):
html = _markdown(text)

View file

@ -714,8 +714,8 @@ def get_filter(doctype, f):
# if operator is missing
f.operator = "="
valid_operators = ("=", "!=", ">", "<", ">=", "<=", "like", "not like", "in", "not in", "Between")
if f.operator not in valid_operators:
valid_operators = ("=", "!=", ">", "<", ">=", "<=", "like", "not like", "in", "not in", "between")
if f.operator.lower() not in valid_operators:
frappe.throw("Operator must be one of {0}".format(", ".join(valid_operators)))

131
frappe/utils/html_utils.py Normal file
View file

@ -0,0 +1,131 @@
import json
import bleach, bleach_whitelist
def sanitize_html(html, linkify=False):
"""
Sanitize HTML tags, attributes and style to prevent XSS attacks
Based on bleach clean, bleach whitelist and HTML5lib's Sanitizer defaults
Does not sanitize JSON, as it could lead to future problems
"""
if not isinstance(html, basestring):
return html
elif is_json(html):
return html
tags = (acceptable_elements + svg_elements + mathml_elements
+ ["html", "head", "meta", "link", "body", "iframe", "style", "o:p"])
attributes = {"*": acceptable_attributes, 'svg': svg_attributes}
styles = bleach_whitelist.all_styles
strip_comments = False
# retuns html with escaped tags, escaped orphan >, <, etc.
escaped_html = bleach.clean(html, tags=tags, attributes=attributes, styles=styles, strip_comments=strip_comments)
if linkify:
escaped_html = bleach.linkify(escaped_html, callbacks=[])
return escaped_html
def is_json(text):
try:
json.loads(text)
except ValueError:
return False
else:
return True
# adapted from https://raw.githubusercontent.com/html5lib/html5lib-python/4aa79f113e7486c7ec5d15a6e1777bfe546d3259/html5lib/sanitizer.py
acceptable_elements = ['a', 'abbr', 'acronym', 'address', 'area',
'article', 'aside', 'audio', 'b', 'big', 'blockquote', 'br', 'button',
'canvas', 'caption', 'center', 'cite', 'code', 'col', 'colgroup',
'command', 'datagrid', 'datalist', 'dd', 'del', 'details', 'dfn',
'dialog', 'dir', 'div', 'dl', 'dt', 'em', 'event-source', 'fieldset',
'figcaption', 'figure', 'footer', 'font', 'form', 'header', 'h1',
'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'img', 'input', 'ins',
'keygen', 'kbd', 'label', 'legend', 'li', 'm', 'map', 'menu', 'meter',
'multicol', 'nav', 'nextid', 'ol', 'output', 'optgroup', 'option',
'p', 'pre', 'progress', 'q', 's', 'samp', 'section', 'select',
'small', 'sound', 'source', 'spacer', 'span', 'strike', 'strong',
'sub', 'sup', 'table', 'tbody', 'td', 'textarea', 'time', 'tfoot',
'th', 'thead', 'tr', 'tt', 'u', 'ul', 'var', 'video']
mathml_elements = ['maction', 'math', 'merror', 'mfrac', 'mi',
'mmultiscripts', 'mn', 'mo', 'mover', 'mpadded', 'mphantom',
'mprescripts', 'mroot', 'mrow', 'mspace', 'msqrt', 'mstyle', 'msub',
'msubsup', 'msup', 'mtable', 'mtd', 'mtext', 'mtr', 'munder',
'munderover', 'none']
svg_elements = ['a', 'animate', 'animateColor', 'animateMotion',
'animateTransform', 'clipPath', 'circle', 'defs', 'desc', 'ellipse',
'font-face', 'font-face-name', 'font-face-src', 'g', 'glyph', 'hkern',
'linearGradient', 'line', 'marker', 'metadata', 'missing-glyph',
'mpath', 'path', 'polygon', 'polyline', 'radialGradient', 'rect',
'set', 'stop', 'svg', 'switch', 'text', 'title', 'tspan', 'use']
acceptable_attributes = ['abbr', 'accept', 'accept-charset', 'accesskey',
'action', 'align', 'alt', 'autocomplete', 'autofocus', 'axis',
'background', 'balance', 'bgcolor', 'bgproperties', 'border',
'bordercolor', 'bordercolordark', 'bordercolorlight', 'bottompadding',
'cellpadding', 'cellspacing', 'ch', 'challenge', 'char', 'charoff',
'choff', 'charset', 'checked', 'cite', 'class', 'clear', 'color',
'cols', 'colspan', 'compact', 'contenteditable', 'controls', 'coords',
'data', 'datafld', 'datapagesize', 'datasrc', 'datetime', 'default',
'delay', 'dir', 'disabled', 'draggable', 'dynsrc', 'enctype', 'end',
'face', 'for', 'form', 'frame', 'galleryimg', 'gutter', 'headers',
'height', 'hidefocus', 'hidden', 'high', 'href', 'hreflang', 'hspace',
'icon', 'id', 'inputmode', 'ismap', 'keytype', 'label', 'leftspacing',
'lang', 'list', 'longdesc', 'loop', 'loopcount', 'loopend',
'loopstart', 'low', 'lowsrc', 'max', 'maxlength', 'media', 'method',
'min', 'multiple', 'name', 'nohref', 'noshade', 'nowrap', 'open',
'optimum', 'pattern', 'ping', 'point-size', 'poster', 'pqg', 'preload',
'prompt', 'radiogroup', 'readonly', 'rel', 'repeat-max', 'repeat-min',
'replace', 'required', 'rev', 'rightspacing', 'rows', 'rowspan',
'rules', 'scope', 'selected', 'shape', 'size', 'span', 'src', 'start',
'step', 'style', 'summary', 'suppress', 'tabindex', 'target',
'template', 'title', 'toppadding', 'type', 'unselectable', 'usemap',
'urn', 'valign', 'value', 'variable', 'volume', 'vspace', 'vrml',
'width', 'wrap', 'xml:lang']
mathml_attributes = ['actiontype', 'align', 'columnalign', 'columnalign',
'columnalign', 'columnlines', 'columnspacing', 'columnspan', 'depth',
'display', 'displaystyle', 'equalcolumns', 'equalrows', 'fence',
'fontstyle', 'fontweight', 'frame', 'height', 'linethickness', 'lspace',
'mathbackground', 'mathcolor', 'mathvariant', 'mathvariant', 'maxsize',
'minsize', 'other', 'rowalign', 'rowalign', 'rowalign', 'rowlines',
'rowspacing', 'rowspan', 'rspace', 'scriptlevel', 'selection',
'separator', 'stretchy', 'width', 'width', 'xlink:href', 'xlink:show',
'xlink:type', 'xmlns', 'xmlns:xlink']
svg_attributes = ['accent-height', 'accumulate', 'additive', 'alphabetic',
'arabic-form', 'ascent', 'attributeName', 'attributeType',
'baseProfile', 'bbox', 'begin', 'by', 'calcMode', 'cap-height',
'class', 'clip-path', 'color', 'color-rendering', 'content', 'cx',
'cy', 'd', 'dx', 'dy', 'descent', 'display', 'dur', 'end', 'fill',
'fill-opacity', 'fill-rule', 'font-family', 'font-size',
'font-stretch', 'font-style', 'font-variant', 'font-weight', 'from',
'fx', 'fy', 'g1', 'g2', 'glyph-name', 'gradientUnits', 'hanging',
'height', 'horiz-adv-x', 'horiz-origin-x', 'id', 'ideographic', 'k',
'keyPoints', 'keySplines', 'keyTimes', 'lang', 'marker-end',
'marker-mid', 'marker-start', 'markerHeight', 'markerUnits',
'markerWidth', 'mathematical', 'max', 'min', 'name', 'offset',
'opacity', 'orient', 'origin', 'overline-position',
'overline-thickness', 'panose-1', 'path', 'pathLength', 'points',
'preserveAspectRatio', 'r', 'refX', 'refY', 'repeatCount',
'repeatDur', 'requiredExtensions', 'requiredFeatures', 'restart',
'rotate', 'rx', 'ry', 'slope', 'stemh', 'stemv', 'stop-color',
'stop-opacity', 'strikethrough-position', 'strikethrough-thickness',
'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap',
'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity',
'stroke-width', 'systemLanguage', 'target', 'text-anchor', 'to',
'transform', 'type', 'u1', 'u2', 'underline-position',
'underline-thickness', 'unicode', 'unicode-range', 'units-per-em',
'values', 'version', 'viewBox', 'visibility', 'width', 'widths', 'x',
'x-height', 'x1', 'x2', 'xlink:actuate', 'xlink:arcrole',
'xlink:href', 'xlink:role', 'xlink:show', 'xlink:title', 'xlink:type',
'xml:base', 'xml:lang', 'xml:space', 'xmlns', 'xmlns:xlink', 'y',
'y1', 'y2', 'zoomAndPan']

View file

@ -261,6 +261,7 @@ def add_system_manager(email, first_name=None, last_name=None, send_welcome_emai
"user_type": "System User",
"send_welcome_email": 1 if send_welcome_email else 0
})
user.insert()
# add roles